diff --git a/account/admin.py b/account/admin.py index cf3ff23..be456ab 100644 --- a/account/admin.py +++ b/account/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin """Import django app""" -from .models import UserEmailOtp, DefaultTaskImages, UserNotification, UserDelete, UserDeviceDetails +from .models import UserEmailOtp, DefaultTaskImages, UserNotification, UserDelete, UserDeviceDetails, ForceUpdate # Register your models here. @admin.register(UserDelete) @@ -39,6 +39,19 @@ class UserEmailOtpAdmin(admin.ModelAdmin): """Return object in email and otp format""" return self.email + '-' + self.otp +@admin.register(ForceUpdate) +class ForceUpdateAdmin(admin.ModelAdmin): + """Force update""" + list_display = ['version', 'device_type'] + readonly_fields = ('device_type',) + + def has_add_permission(self, request): + count = ForceUpdate.objects.all().count() + if count < 2: + return True + return False + def has_delete_permission(self, request, obj=None): + return False @admin.register(UserDeviceDetails) class UserDeviceDetailsAdmin(admin.ModelAdmin): """User profile admin""" diff --git a/account/custom_middleware.py b/account/custom_middleware.py index 7a06e43..b3cc750 100644 --- a/account/custom_middleware.py +++ b/account/custom_middleware.py @@ -5,9 +5,11 @@ from rest_framework.response import Response from rest_framework.renderers import JSONRenderer """App django""" from account.utils import custom_error_response -from account.models import UserDeviceDetails +from account.models import UserDeviceDetails, ForceUpdate from base.messages import ERROR_CODE, SUCCESS_CODE - +from base.constants import NUMBER +from junior.models import Junior +from guardian.models import Guardian # Custom middleware # when user login with # multiple device simultaneously @@ -15,6 +17,18 @@ from base.messages import ERROR_CODE, SUCCESS_CODE # multiple devices only # user can login in single # device at a time""" +# force update +# use 308 status code for force update + +def custom_response(custom_error, response_status = status.HTTP_404_NOT_FOUND): + """custom response""" + response = Response(custom_error.data, status=response_status) + # Set content type header to "application/json" + response['Content-Type'] = 'application/json' + # Render the response as JSON + renderer = JSONRenderer() + response.content = renderer.render(response.data) + return response class CustomMiddleware(object): """Custom middleware""" def __init__(self, get_response): @@ -26,15 +40,33 @@ class CustomMiddleware(object): response = self.get_response(request) # Code to be executed after the view is called device_id = request.META.get('HTTP_DEVICE_ID') + user_type = request.META.get('HTTP_USER_TYPE') + version = request.META.get('HTTP_VERSION') + device_type = str(request.META.get('HTTP_TYPE')) + + api_endpoint = request.path if request.user.is_authenticated: - """device details""" - device_details = UserDeviceDetails.objects.filter(user=request.user, device_id=device_id).last() - if device_id and not device_details: - custom_error = custom_error_response(ERROR_CODE['2037'], response_status=status.HTTP_404_NOT_FOUND) - response = Response(custom_error.data, status=status.HTTP_404_NOT_FOUND) - # Set content type header to "application/json" - response['Content-Type'] = 'application/json' - # Render the response as JSON - renderer = JSONRenderer() - response.content = renderer.render(response.data) + # device details + if device_id: + device_details = UserDeviceDetails.objects.filter(user=request.user, device_id=device_id).last() + if not device_details and api_endpoint != '/api/v1/user/login/': + custom_error = custom_error_response(ERROR_CODE['2037'], response_status=status.HTTP_404_NOT_FOUND) + response = custom_response(custom_error) + if user_type and str(user_type) == str(NUMBER['one']): + junior = Junior.objects.filter(auth=request.user, is_active=False).last() + if junior: + custom_error = custom_error_response(ERROR_CODE['2075'], response_status=status.HTTP_404_NOT_FOUND) + response = custom_response(custom_error) + elif user_type and str(user_type) == str(NUMBER['two']): + guardian = Guardian.objects.filter(user=request.user, is_active=False).last() + if guardian: + custom_error = custom_error_response(ERROR_CODE['2075'], response_status=status.HTTP_404_NOT_FOUND) + response = custom_response(custom_error) + + if version and device_type: + force_update = ForceUpdate.objects.filter(version=version, device_type=device_type).last() + if not force_update: + custom_error = custom_error_response(ERROR_CODE['2079'], + response_status=status.HTTP_308_PERMANENT_REDIRECT) + response = custom_response(custom_error, status.HTTP_308_PERMANENT_REDIRECT) return response diff --git a/account/migrations/0010_forceupdate.py b/account/migrations/0010_forceupdate.py new file mode 100644 index 0000000..84f5b12 --- /dev/null +++ b/account/migrations/0010_forceupdate.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.2 on 2023-08-22 07:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0009_alter_userdevicedetails_device_id'), + ] + + operations = [ + migrations.CreateModel( + name='ForceUpdate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, max_length=50, null=True)), + ('device_type', models.CharField(blank=True, choices=[('1', 'android'), ('2', 'ios')], default=None, max_length=15, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Force Update Version', + 'verbose_name_plural': 'Force Update Version', + 'db_table': 'force_update', + }, + ), + ] diff --git a/account/models.py b/account/models.py index 784a60e..d13762b 100644 --- a/account/models.py +++ b/account/models.py @@ -2,8 +2,9 @@ """Django import""" from django.db import models from django.contrib.auth.models import User +from django.core.exceptions import ValidationError """App import""" -from base.constants import USER_TYPE +from base.constants import USER_TYPE, DEVICE_TYPE # Create your models here. class UserProfile(models.Model): @@ -165,3 +166,25 @@ class UserDeviceDetails(models.Model): def __str__(self): return self.user.email + + + + +class ForceUpdate(models.Model): + """ + Force update + """ + """Version ID""" + version = models.CharField(max_length=50, null=True, blank=True) + device_type = models.CharField(max_length=15, choices=DEVICE_TYPE, null=True, blank=True, default=None) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta(object): + """ Meta information """ + db_table = 'force_update' + verbose_name = 'Force Update Version' + verbose_name_plural = 'Force Update Version' + + def __str__(self): + return self.version diff --git a/account/serializers.py b/account/serializers.py index ebdc263..b783efd 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -18,7 +18,7 @@ import secrets from guardian.models import Guardian from junior.models import Junior -from account.models import UserEmailOtp, DefaultTaskImages, UserDelete, UserNotification, UserPhoneOtp +from account.models import UserEmailOtp, DefaultTaskImages, UserDelete, UserNotification, UserPhoneOtp, ForceUpdate from base.constants import GUARDIAN, JUNIOR, SUPERUSER, NUMBER from base.messages import ERROR_CODE, SUCCESS_CODE, STATUS_CODE_ERROR from .utils import delete_user_account_condition_social, delete_user_account_condition @@ -104,10 +104,12 @@ class ResetPasswordSerializer(serializers.Serializer): return user_opt_details return '' + class ChangePasswordSerializer(serializers.Serializer): """Update Password after verification""" - current_password = serializers.CharField(max_length=100) + current_password = serializers.CharField(max_length=100, required=True) new_password = serializers.CharField(required=True) + class Meta(object): """Meta info""" model = User @@ -118,25 +120,36 @@ class ChangePasswordSerializer(serializers.Serializer): if self.context.password not in ('', None) and user.check_password(value): return value raise serializers.ValidationError(ERROR_CODE['2015']) + def create(self, validated_data): + """ + change password + """ new_password = validated_data.pop('new_password') current_password = validated_data.pop('current_password') - """Check new password is different from current password""" + # Check new password is different from current password if new_password == current_password: raise serializers.ValidationError({"details": ERROR_CODE['2026']}) - user_details = User.objects.filter(email=self.context).last() - if user_details: - user_details.set_password(new_password) - user_details.save() - return {'password':new_password} - return '' + user_details = self.context + user_details.set_password(new_password) + user_details.save() + return {'password':new_password} class ForgotPasswordSerializer(serializers.Serializer): """Forget password serializer""" - email = serializers.EmailField() + email = serializers.EmailField(required=True) + def validate_email(self, value): + """ + validate email exist ot not + value: string + return none + """ + if not User.objects.get(email=value): + raise serializers.ValidationError({'details': ERROR_CODE['2004']}) + return value class AdminLoginSerializer(serializers.ModelSerializer): """admin login serializer""" @@ -244,7 +257,7 @@ class GuardianSerializer(serializers.ModelSerializer): """Meta info""" model = Guardian fields = ['id', 'auth_token', 'refresh_token', 'email', 'first_name', 'last_name', 'country_code', - 'phone', 'family_name', 'gender', 'dob', 'referral_code', 'is_active', + 'phone', 'family_name', 'gender', 'dob', 'referral_code', 'is_active', 'is_deleted', 'is_complete_profile', 'passcode', 'image', 'created_at', 'updated_at', 'user_type', 'country_name'] @@ -287,14 +300,15 @@ class JuniorSerializer(serializers.ModelSerializer): model = Junior fields = ['id', 'auth_token', 'refresh_token', 'email', 'first_name', 'last_name', 'country_code', 'phone', 'gender', 'dob', 'guardian_code', 'referral_code','is_active', 'is_password_set', - 'is_complete_profile', 'created_at', 'image', 'updated_at', 'user_type', 'country_name','is_invited'] + 'is_complete_profile', 'created_at', 'image', 'updated_at', 'user_type', 'country_name','is_invited', + 'is_deleted'] class EmailVerificationSerializer(serializers.ModelSerializer): """Email verification serializer""" class Meta(object): """Meta info""" model = UserEmailOtp - fields = '__all__' + fields = ('email',) @@ -376,3 +390,12 @@ class UserPhoneOtpSerializer(serializers.ModelSerializer): """Meta info""" model = UserPhoneOtp fields = '__all__' + +class ForceUpdateSerializer(serializers.ModelSerializer): + """ ForceUpdate Serializer + """ + + class Meta(object): + """ meta info """ + model = ForceUpdate + fields = ('id', 'version', 'device_type') diff --git a/account/templates/templated_email/support_mail.email b/account/templates/templated_email/support_mail.email index 50467a9..34d6156 100644 --- a/account/templates/templated_email/support_mail.email +++ b/account/templates/templated_email/support_mail.email @@ -8,14 +8,14 @@

- Hi {{name}}, + Hi Support Team,

- {{name}} have some queries and need some support. Please support them by using their email address {{sender}}.

Queries are:-
{{ message }} + {{name}} have some queries and need some support. Please support them by using their email address {{sender}}.

Queries are:-

  • {{ message }}
  • diff --git a/account/templates/templated_email/user_deactivate.email b/account/templates/templated_email/user_deactivate.email new file mode 100644 index 0000000..90b3ee1 --- /dev/null +++ b/account/templates/templated_email/user_deactivate.email @@ -0,0 +1,22 @@ +{% extends "templated_email/email_base.email" %} + +{% block subject %} + Account Deactivated +{% endblock %} + +{% block plain %} + + +

    + Hi User, +

    + + + + +

    + Your account has been deactivated by admin. Please reach out to the admin for assistance. +

    + + +{% endblock %} diff --git a/account/urls.py b/account/urls.py index 02ac124..4944d67 100644 --- a/account/urls.py +++ b/account/urls.py @@ -29,7 +29,7 @@ from .views import (UserLogin, SendPhoneOtp, UserPhoneVerification, UserEmailVer GoogleLoginViewSet, SigninWithApple, ProfileAPIViewSet, UploadImageAPIViewSet, DefaultImageAPIViewSet, DeleteUserProfileAPIViewSet, UserNotificationAPIViewSet, UpdateUserNotificationAPIViewSet, SendSupportEmail, LogoutAPIView, AccessTokenAPIView, - AdminLoginViewSet) + AdminLoginViewSet, ForceUpdateViewSet) """Router""" router = routers.SimpleRouter() @@ -39,8 +39,6 @@ router.register('user', UserLogin, basename='user') router.register('admin', AdminLoginViewSet, basename='admin') """google login end point""" router.register('google-login', GoogleLoginViewSet, basename='admin') -router.register('send-phone-otp', SendPhoneOtp, basename='send-phone-otp') -router.register('user-phone-verification', UserPhoneVerification, basename='user-phone-verification') """email verification end point""" router.register('user-email-verification', UserEmailVerification, basename='user-email-verification') """Resend email otp end point""" @@ -57,6 +55,8 @@ router.register('delete', DeleteUserProfileAPIViewSet, basename='delete') router.register('user-notification', UserNotificationAPIViewSet, basename='user-notification') """update user account notification""" router.register('update-user-notification', UpdateUserNotificationAPIViewSet, basename='update-user-notification') +# Force update entry API +router.register('force-update', ForceUpdateViewSet, basename='force-update') """Define url pattern""" urlpatterns = [ path('api/v1/', include(router.urls)), diff --git a/account/utils.py b/account/utils.py index 52c016a..60a5c44 100644 --- a/account/utils.py +++ b/account/utils.py @@ -93,8 +93,9 @@ def junior_account_update(user_tb): # Update junior account junior_data.is_active = False junior_data.is_verified = False - junior_data.guardian_code = '{}' - junior_data.guardian_code_status = str(NUMBER['one']) + junior_data.guardian_code = None + junior_data.guardian_code_status = None + junior_data.is_deleted = True junior_data.save() JuniorPoints.objects.filter(junior=junior_data).delete() @@ -105,6 +106,7 @@ def guardian_account_update(user_tb): # Update guardian account guardian_data.is_active = False guardian_data.is_verified = False + guardian_data.is_deleted = True guardian_data.save() jun_data = Junior.objects.filter(guardian_code__icontains=str(guardian_data.guardian_code)) """Disassociate relation between guardian and junior""" @@ -127,6 +129,28 @@ def send_otp_email(recipient_email, otp): ) return otp + +@shared_task() +def send_all_email(template_name, email, otp): + """ + Send all type of email by passing template name + template_name: string + email: string + otp: string + """ + from_email = settings.EMAIL_FROM_ADDRESS + recipient_list = [email] + send_templated_mail( + template_name=template_name, + from_email=from_email, + recipient_list=recipient_list, + context={ + 'verification_code': otp + } + ) + + return otp + @shared_task def user_device_details(user, device_id): """ @@ -135,10 +159,12 @@ def user_device_details(user, device_id): device_id: string return """ - device_details, created = UserDeviceDetails.objects.get_or_create(user=user) + device_details, created = UserDeviceDetails.objects.get_or_create(user__id=user) if device_details: device_details.device_id = device_id device_details.save() + return True + return False def send_support_email(name, sender, subject, message): @@ -178,8 +204,12 @@ def custom_error_response(detail, response_status): if not detail: """when details is empty""" detail = {} - return Response({"error": detail, "status": "failed", "code": response_status}) - + if response_status == 406: + return Response({"error": detail, "status": "failed", "code": response_status,}, + status=status.HTTP_308_PERMANENT_REDIRECT) + else: + return Response({"error": detail, "status": "failed", "code": response_status}, + status=status.HTTP_400_BAD_REQUEST) def get_user_data(attrs): """ @@ -251,3 +281,10 @@ def generate_code(value, user_id): OTP_EXPIRY = timezone.now() + timezone.timedelta(days=1) + + +def get_user_full_name(user_obj): + """ + to get user's full name + """ + return f"{user_obj.first_name} {user_obj.last_name}" if user_obj.first_name or user_obj.last_name else "User" diff --git a/account/views.py b/account/views.py index 227730e..e208163 100644 --- a/account/views.py +++ b/account/views.py @@ -4,6 +4,7 @@ import threading from notifications.utils import remove_fcm_token # django imports +from rest_framework.viewsets import GenericViewSet, mixins from datetime import datetime, timedelta from rest_framework import viewsets, status, views from rest_framework.decorators import action @@ -26,20 +27,21 @@ from django.conf import settings from guardian.models import Guardian from junior.models import Junior, JuniorPoints from guardian.utils import upload_image_to_alibaba -from account.models import UserDeviceDetails, UserPhoneOtp, UserEmailOtp, DefaultTaskImages, UserNotification +from account.models import (UserDeviceDetails, UserPhoneOtp, UserEmailOtp, DefaultTaskImages, UserNotification, + ForceUpdate) from django.contrib.auth.models import User from .serializers import (SuperUserSerializer, GuardianSerializer, JuniorSerializer, EmailVerificationSerializer, ForgotPasswordSerializer, ResetPasswordSerializer, ChangePasswordSerializer, GoogleLoginSerializer, UpdateGuardianImageSerializer, UpdateJuniorProfileImageSerializer, DefaultTaskImagesSerializer, DefaultTaskImagesDetailsSerializer, UserDeleteSerializer, UserNotificationSerializer, UpdateUserNotificationSerializer, UserPhoneOtpSerializer, - AdminLoginSerializer) + AdminLoginSerializer, ForceUpdateSerializer) from rest_framework_simplejwt.tokens import RefreshToken from base.messages import ERROR_CODE, SUCCESS_CODE from base.constants import NUMBER, ZOD, JUN, GRD, USER_TYPE_FLAG from guardian.tasks import generate_otp from account.utils import (send_otp_email, send_support_email, custom_response, custom_error_response, - generate_code, OTP_EXPIRY, user_device_details) + generate_code, OTP_EXPIRY, user_device_details, send_all_email) from junior.serializers import JuniorProfileSerializer from guardian.serializers import GuardianProfileSerializer @@ -49,7 +51,8 @@ class GoogleLoginMixin(object): def google_login(request): """google login function""" access_token = request.data.get('access_token') - user_type = request.data.get('user_type') + user_type = request.META.get('HTTP_USER_TYPE') + device_id = request.META.get('HTTP_DEVICE_ID') if not access_token: return Response({'error': 'Access token is required.'}, status=status.HTTP_400_BAD_REQUEST) @@ -82,14 +85,29 @@ class GoogleLoginMixin(object): if user_data.exists(): if str(user_type) == '1': junior_query = Junior.objects.filter(auth=user_data.last()).last() + if not junior_query: + return custom_error_response( + ERROR_CODE["2071"], + response_status=status.HTTP_400_BAD_REQUEST + ) serializer = JuniorSerializer(junior_query) - if str(user_type) == '2': + elif str(user_type) == '2': guardian_query = Guardian.objects.filter(user=user_data.last()).last() + if not guardian_query: + return custom_error_response( + ERROR_CODE["2070"], + response_status=status.HTTP_400_BAD_REQUEST + ) serializer = GuardianSerializer(guardian_query) + else: + return custom_error_response( + ERROR_CODE["2069"], + response_status=status.HTTP_400_BAD_REQUEST + ) return custom_response(SUCCESS_CODE['3003'], serializer.data, response_status=status.HTTP_200_OK) - if not User.objects.filter(email__iexact=email).exists(): + else: user_obj = User.objects.create(username=email, email=email, first_name=first_name, last_name=last_name) if str(user_type) == '1': junior_query = Junior.objects.create(auth=user_obj, is_verified=True, is_active=True, @@ -100,13 +118,23 @@ class GoogleLoginMixin(object): serializer = JuniorSerializer(junior_query) position = Junior.objects.all().count() JuniorPoints.objects.create(junior=junior_query, position=position) - if str(user_type) == '2': + elif str(user_type) == '2': guardian_query = Guardian.objects.create(user=user_obj, is_verified=True, is_active=True, image=profile_picture,signup_method='2', guardian_code=generate_code(GRD, user_obj.id), referral_code=generate_code(ZOD, user_obj.id) ) serializer = GuardianSerializer(guardian_query) + else: + user_obj.delete() + return custom_error_response( + ERROR_CODE["2069"], + response_status=status.HTTP_400_BAD_REQUEST + ) + device_detail, created = UserDeviceDetails.objects.get_or_create(user=user_obj) + if device_detail: + device_detail.device_id = device_id + device_detail.save() # Return a JSON response with the user's email and name return custom_response(SUCCESS_CODE['3003'], serializer.data, response_status=status.HTTP_200_OK) @@ -117,16 +145,26 @@ class GoogleLoginViewSet(GoogleLoginMixin, viewsets.GenericViewSet): serializer_class = GoogleLoginSerializer def create(self, request): - """create method""" + """Payload + { + "access_token", + "user_type": "1" + }""" serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) return self.google_login(request) class SigninWithApple(views.APIView): - """This API is for sign in with Apple for app.""" + """This API is for sign in with Apple for app. + Payload + { + "access_token", + "user_type": "1" + }""" def post(self, request): token = request.data.get("access_token") - user_type = request.data.get("user_type") + user_type = request.META.get('HTTP_USER_TYPE') + device_id = request.META.get('HTTP_DEVICE_ID') try: decoded_data = jwt.decode(token, options={"verify_signature": False}) user_data = {"email": decoded_data.get('email'), "username": decoded_data.get('email'), "is_active": True} @@ -134,11 +172,26 @@ class SigninWithApple(views.APIView): try: user = User.objects.get(email=decoded_data.get("email")) if str(user_type) == '1': - junior_query = Junior.objects.filter(auth=user).last() - serializer = JuniorSerializer(junior_query) - if str(user_type) == '2': - guardian_query = Guardian.objects.filter(user=user).last() - serializer = GuardianSerializer(guardian_query) + junior_data = Junior.objects.filter(auth=user).last() + if not junior_data: + return custom_error_response( + ERROR_CODE["2071"], + response_status=status.HTTP_400_BAD_REQUEST + ) + serializer = JuniorSerializer(junior_data) + elif str(user_type) == '2': + guardian_data = Guardian.objects.filter(user=user).last() + if not guardian_data: + return custom_error_response( + ERROR_CODE["2070"], + response_status=status.HTTP_400_BAD_REQUEST + ) + serializer = GuardianSerializer(guardian_data) + else: + return custom_error_response( + ERROR_CODE["2069"], + response_status=status.HTTP_400_BAD_REQUEST + ) return custom_response(SUCCESS_CODE['3003'], serializer.data, response_status=status.HTTP_200_OK) @@ -152,12 +205,22 @@ class SigninWithApple(views.APIView): serializer = JuniorSerializer(junior_query) position = Junior.objects.all().count() JuniorPoints.objects.create(junior=junior_query, position=position) - if str(user_type) == '2': + elif str(user_type) == '2': guardian_query = Guardian.objects.create(user=user, is_verified=True, is_active=True, signup_method='3', guardian_code=generate_code(GRD, user.id), referral_code=generate_code(ZOD, user.id)) serializer = GuardianSerializer(guardian_query) + else: + user.delete() + return custom_error_response( + ERROR_CODE["2069"], + response_status=status.HTTP_400_BAD_REQUEST + ) + device_detail, created = UserDeviceDetails.objects.get_or_create(user=user) + if device_detail: + device_detail.device_id = device_id + device_detail.save() return custom_response(SUCCESS_CODE['3003'], serializer.data, response_status=status.HTTP_200_OK) except Exception as e: @@ -193,18 +256,42 @@ class UpdateProfileImage(views.APIView): return custom_error_response(ERROR_CODE['2036'],response_status=status.HTTP_400_BAD_REQUEST) class ChangePasswordAPIView(views.APIView): - """change password""" + """ + change password" + """ serializer_class = ChangePasswordSerializer permission_classes = [IsAuthenticated] + def post(self, request): - serializer = ChangePasswordSerializer(context=request.user, data=request.data) + """ + POST request to change current login user password + Payload + { "current_password":"Demo@123", + "new_password":"Demo@123" + } + """ + serializer = ChangePasswordSerializer( + context=request.user, + data=request.data + ) if serializer.is_valid(): serializer.save() - return custom_response(SUCCESS_CODE['3007'], response_status=status.HTTP_200_OK) - return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST) + return custom_response( + SUCCESS_CODE['3007'], + response_status=status.HTTP_200_OK + ) + return custom_error_response( + serializer.errors, + response_status=status.HTTP_400_BAD_REQUEST + ) class ResetPasswordAPIView(views.APIView): - """Reset password""" + """Reset password + Payload + { + "verification_code":"373770", + "password":"Demo@1323" + }""" def post(self, request): serializer = ResetPasswordSerializer(data=request.data) if serializer.is_valid(): @@ -213,40 +300,43 @@ class ResetPasswordAPIView(views.APIView): return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST) class ForgotPasswordAPIView(views.APIView): - """Forgot password""" + """ + Forgot password + """ + serializer_class = ForgotPasswordSerializer def post(self, request): - serializer = ForgotPasswordSerializer(data=request.data) - if serializer.is_valid(): - email = serializer.validated_data['email'] - try: - User.objects.get(email=email) - except User.DoesNotExist: - return custom_error_response(ERROR_CODE['2004'], response_status=status.HTTP_404_NOT_FOUND) - verification_code = generate_otp() - # Send the verification code to the user's email - from_email = settings.EMAIL_FROM_ADDRESS - recipient_list = [email] - send_templated_mail( - template_name='email_reset_verification.email', - from_email=from_email, - recipient_list=recipient_list, - context={ - 'verification_code': verification_code - } - ) - expiry = OTP_EXPIRY - user_data, created = UserEmailOtp.objects.get_or_create(email=email) - if created: - user_data.expired_at = expiry - user_data.save() - if user_data: - user_data.otp = verification_code - user_data.expired_at = expiry - user_data.save() - return custom_response(SUCCESS_CODE['3015'], - response_status=status.HTTP_200_OK) - return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST) + + """ + Payload + { + "email": "abc@yopmail.com" + } + """ + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + email = serializer.validated_data['email'] + # generate otp + verification_code = generate_otp() + # Send the verification code to the user's email + send_all_email.delay( + 'email_reset_verification.email', email, verification_code + ) + expiry = OTP_EXPIRY + user_data, created = UserEmailOtp.objects.get_or_create( + email=email + ) + if created: + user_data.expired_at = expiry + user_data.save() + if user_data: + user_data.otp = verification_code + user_data.expired_at = expiry + user_data.save() + return custom_response( + SUCCESS_CODE['3015'], + response_status=status.HTTP_200_OK + ) class SendPhoneOtp(viewsets.ModelViewSet): """Send otp on phone""" @@ -287,7 +377,7 @@ class UserLogin(viewsets.ViewSet): def login(self, request): username = request.data.get('username') password = request.data.get('password') - user_type = request.data.get('user_type') + user_type = request.META.get('HTTP_USER_TYPE') device_id = request.META.get('HTTP_DEVICE_ID') user = authenticate(request, username=username, password=password) @@ -295,22 +385,24 @@ class UserLogin(viewsets.ViewSet): if user is not None: login(request, user) if str(user_type) == USER_TYPE_FLAG["TWO"]: - guardian_data = Guardian.objects.filter(user__username=username, is_verified=True).last() + guardian_data = Guardian.objects.filter(user__username=username).last() if guardian_data: - serializer = GuardianSerializer( - guardian_data, context={'user_type': user_type} - ).data + if guardian_data.is_verified: + serializer = GuardianSerializer( + guardian_data, context={'user_type': user_type} + ).data else: return custom_error_response( ERROR_CODE["2070"], response_status=status.HTTP_401_UNAUTHORIZED ) elif str(user_type) == USER_TYPE_FLAG["FIRST"]: - junior_data = Junior.objects.filter(auth__username=username, is_verified=True).last() + junior_data = Junior.objects.filter(auth__username=username).last() if junior_data: - serializer = JuniorSerializer( - junior_data, context={'user_type': user_type} - ).data + if junior_data.is_verified: + serializer = JuniorSerializer( + junior_data, context={'user_type': user_type} + ).data else: return custom_error_response( ERROR_CODE["2071"], @@ -321,8 +413,12 @@ class UserLogin(viewsets.ViewSet): ERROR_CODE["2069"], response_status=status.HTTP_401_UNAUTHORIZED ) - # storing device id in using thread so the time would be reduced - threading.Thread(target=user_device_details, args=(user, device_id)) + # storing device id in using celery task so the time would be reduced + # user_device_details.delay(user.id, device_id) + device_details, created = UserDeviceDetails.objects.get_or_create(user=user) + if device_details: + device_details.device_id = device_id + device_details.save() return custom_response(SUCCESS_CODE['3003'], serializer, response_status=status.HTTP_200_OK) else: return custom_error_response(ERROR_CODE["2002"], response_status=status.HTTP_401_UNAUTHORIZED) @@ -399,14 +495,20 @@ class AdminLoginViewSet(viewsets.GenericViewSet): class UserEmailVerification(viewsets.ModelViewSet): - """User Email verification""" + """User Email verification + Payload + { + "email":"ramu@yopmail.com", + "otp":"361123" + }""" serializer_class = EmailVerificationSerializer + http_method_names = ('post',) - def list(self, request, *args, **kwargs): + def create(self, request, *args, **kwargs): try: - user_obj = User.objects.filter(username=self.request.GET.get('email')).last() - email_data = UserEmailOtp.objects.filter(email=self.request.GET.get('email'), - otp=self.request.GET.get('otp')).last() + user_obj = User.objects.filter(username=self.request.data.get('email')).last() + email_data = UserEmailOtp.objects.filter(email=self.request.data.get('email'), + otp=self.request.data.get('otp')).last() if email_data: input_datetime_str = str(email_data.expired_at) input_format = "%Y-%m-%d %H:%M:%S.%f%z" @@ -420,12 +522,12 @@ class UserEmailVerification(viewsets.ModelViewSet): email_data.is_verified = True email_data.save() if email_data.user_type == '1': - junior_data = Junior.objects.filter(auth__email=self.request.GET.get('email')).last() + junior_data = Junior.objects.filter(auth__email=self.request.data.get('email')).last() if junior_data: junior_data.is_verified = True junior_data.save() else: - guardian_data = Guardian.objects.filter(user__email=self.request.GET.get('email')).last() + guardian_data = Guardian.objects.filter(user__email=self.request.data.get('email')).last() if guardian_data: guardian_data.is_verified = True guardian_data.save() @@ -444,12 +546,14 @@ class UserEmailVerification(viewsets.ModelViewSet): class ReSendEmailOtp(viewsets.ModelViewSet): """Send otp on phone""" serializer_class = EmailVerificationSerializer - permission_classes = [IsAuthenticated] - + http_method_names = ('post',) def create(self, request, *args, **kwargs): + """Param + {"email":"ashok@yopmail.com"} + """ otp = generate_otp() if User.objects.filter(email=request.data['email']): - expiry = OTP_EXPIRY + expiry = timezone.now() + timezone.timedelta(days=1) email_data, created = UserEmailOtp.objects.get_or_create(email=request.data['email']) if created: email_data.expired_at = expiry @@ -458,7 +562,7 @@ class ReSendEmailOtp(viewsets.ModelViewSet): email_data.otp = otp email_data.expired_at = expiry email_data.save() - send_otp_email(request.data['email'], otp) + send_otp_email.delay(request.data['email'], otp) return custom_response(SUCCESS_CODE['3016'], response_status=status.HTTP_200_OK) else: return custom_error_response(ERROR_CODE["2023"], response_status=status.HTTP_400_BAD_REQUEST) @@ -467,23 +571,28 @@ class ProfileAPIViewSet(viewsets.ModelViewSet): """Profile viewset""" serializer_class = JuniorProfileSerializer permission_classes = [IsAuthenticated] + http_method_names = ('get',) def list(self, request, *args, **kwargs): - """profile view""" - if str(self.request.GET.get('user_type')) == '1': + """profile view + Params + user_type""" + user_type = request.META.get('HTTP_USER_TYPE') + if str(user_type) == '1': junior_data = Junior.objects.filter(auth=self.request.user).last() if junior_data: serializer = JuniorProfileSerializer(junior_data) return custom_response(None, serializer.data, response_status=status.HTTP_200_OK) - elif str(self.request.GET.get('user_type')) == '2': + elif str(user_type) == '2': guardian_data = Guardian.objects.filter(user=self.request.user).last() if guardian_data: serializer = GuardianProfileSerializer(guardian_data) return custom_response(None, serializer.data, response_status=status.HTTP_200_OK) - + return custom_error_response(None, response_status=status.HTTP_400_BAD_REQUEST) class UploadImageAPIViewSet(viewsets.ModelViewSet): """upload task image""" serializer_class = DefaultTaskImagesSerializer + http_method_names = ('post',) def create(self, request, *args, **kwargs): """upload images""" image_data = request.data['image_url'] @@ -503,6 +612,7 @@ class DefaultImageAPIViewSet(viewsets.ModelViewSet): """Profile viewset""" serializer_class = DefaultTaskImagesDetailsSerializer permission_classes = [IsAuthenticated] + http_method_names = ('get',) def list(self, request, *args, **kwargs): """profile view""" queryset = DefaultTaskImages.objects.all() @@ -511,7 +621,13 @@ class DefaultImageAPIViewSet(viewsets.ModelViewSet): class DeleteUserProfileAPIViewSet(viewsets.GenericViewSet): - """ Delete user API view set """ + """ Delete user API view set + {"user_type":1, + "signup_method":"1", + "password":"Demo@123"} + signup_method 1 for manual + 2 for google login + 3 for apple login""" @action(detail=False, methods=['POST'], url_path='user-account',serializer_class=UserDeleteSerializer, permission_classes=[IsAuthenticated]) @@ -533,8 +649,9 @@ class UserNotificationAPIViewSet(viewsets.ModelViewSet): """notification viewset""" serializer_class = UserNotificationSerializer permission_classes = [IsAuthenticated] + http_method_names = ('get',) def list(self, request, *args, **kwargs): - """profile view""" + """notification view""" queryset = UserNotification.objects.filter(user=request.user) serializer = UserNotificationSerializer(queryset, many=True) return custom_response(None, serializer.data, response_status=status.HTTP_200_OK) @@ -544,9 +661,14 @@ class UpdateUserNotificationAPIViewSet(viewsets.ModelViewSet): """Update notification viewset""" serializer_class = UpdateUserNotificationSerializer permission_classes = [IsAuthenticated] + http_method_names = ('post',) def create(self, request, *args, **kwargs): - """profile view""" + """Payload + {"email_notification": false, + "sms_notification": false, + "push_notification": false} + """ serializer = UpdateUserNotificationSerializer(data=request.data, context=request.user) if serializer.is_valid(): @@ -556,7 +678,12 @@ class UpdateUserNotificationAPIViewSet(viewsets.ModelViewSet): class SendSupportEmail(views.APIView): - """support email api""" + """support email api + payload + name + email + message + """ permission_classes = (IsAuthenticated,) def post(self, request): @@ -600,3 +727,27 @@ class AccessTokenAPIView(views.APIView): data = {"auth_token": access_token} return custom_response(None, data, response_status=status.HTTP_200_OK) + +class ForceUpdateViewSet(GenericViewSet, mixins.CreateModelMixin): + """FAQ view set""" + + serializer_class = ForceUpdateSerializer + http_method_names = ['post'] + + + def create(self, request, *args, **kwargs): + """ + faq create api method + :param request: + :param args: version, device type + :param kwargs: + :return: success message + """ + if ForceUpdate.objects.all().count() >= 2: + return custom_error_response(ERROR_CODE['2080'], response_status=status.HTTP_400_BAD_REQUEST) + obj_data = [ForceUpdate(**item) for item in request.data] + try: + ForceUpdate.objects.bulk_create(obj_data) + return custom_response(SUCCESS_CODE["3046"], response_status=status.HTTP_200_OK) + except Exception as e: + return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) diff --git a/base/constants.py b/base/constants.py index 9688c0f..da203ae 100644 --- a/base/constants.py +++ b/base/constants.py @@ -27,7 +27,7 @@ NUMBER = { 'ninety_nine': 99, 'hundred': 100, 'thirty_six_hundred': 3600 } - +none = "none" # Super Admin string constant for 'role' SUPER_ADMIN = "Super Admin" @@ -43,14 +43,18 @@ FILE_SIZE = 5 * 1024 * 1024 # String constant for configurable date for allocation lock period ALLOCATION_LOCK_DATE = 1 - +# guardian code status tuple +guardian_code_tuple = ('1','3') """user type""" USER_TYPE = ( ('1', 'junior'), ('2', 'guardian'), ('3', 'superuser') ) - +DEVICE_TYPE = ( + ('1', 'android'), + ('2', 'ios') +) USER_TYPE_FLAG = { "FIRST" : "1", "TWO" : "2", diff --git a/base/messages.py b/base/messages.py index 10fbfaa..54c427f 100644 --- a/base/messages.py +++ b/base/messages.py @@ -96,8 +96,22 @@ ERROR_CODE = { "2067": "Action not allowed. User type missing.", "2068": "No guardian associated with this junior", "2069": "Invalid user type", - "2070": "You did not find as a guardian", - "2071": "You did not find as a junior" + "2070": "You do not find as a guardian", + "2071": "You do not find as a junior", + "2072": "You can not approve or reject this task because junior does not exist in the system", + "2073": "You can not approve or reject this junior because junior does not exist in the system", + "2074": "You can not complete this task because you does not exist in the system", + # deactivate account + "2075": "Your account is deactivated. Please contact with admin", + "2076": "This junior already associate with you", + "2077": "You can not add guardian", + "2078": "This junior is not associate with you", + # force update + "2079": "Please update your app version for enjoying uninterrupted services", + "2080": "Can not add App version", + "2081": "A junior can only be associated with a maximum of 3 guardian", + # guardian code not exist + "2082": "Guardian code does not exist" } """Success message code""" @@ -163,6 +177,9 @@ SUCCESS_CODE = { "3043": "Read article card successfully", # remove guardian code request "3044": "Remove guardian code request successfully", + # create faq + "3045": "Create FAQ data", + "3046": "Add App version successfully" } """status code error""" diff --git a/base/tasks.py b/base/tasks.py index 791be81..fbeca1d 100644 --- a/base/tasks.py +++ b/base/tasks.py @@ -1,29 +1,88 @@ """ web_admin tasks file """ +import datetime + # third party imports from celery import shared_task from templated_email import send_templated_mail # django imports from django.conf import settings +from django.db.models import F, Window +from django.db.models.functions.window import Rank + +# local imports +from base.constants import PENDING, IN_PROGRESS, JUNIOR +from guardian.models import JuniorTask +from junior.models import JuniorPoints +from notifications.constants import PENDING_TASK_EXPIRING, IN_PROGRESS_TASK_EXPIRING, NOTIFICATION_DICT, TOP_JUNIOR +from notifications.models import Notification +from notifications.utils import send_notification, get_from_user_details, send_notification_multiple_user @shared_task -def send_email_otp(email, verification_code): +def send_email(recipient_list, template, context: dict = None): """ used to send otp on email - :param email: e-mail - :param verification_code: otp + :param context: + :param recipient_list: e-mail list + :param template: email template """ + if context is None: + context = {} from_email = settings.EMAIL_FROM_ADDRESS - recipient_list = [email] send_templated_mail( - template_name='email_reset_verification.email', + template_name=template, from_email=from_email, recipient_list=recipient_list, - context={ - 'verification_code': verification_code - } + context=context ) return True + + +@shared_task() +def notify_task_expiry(): + """ + task to send notification for those task which expiring soon + :return: + """ + all_pending_tasks = JuniorTask.objects.filter( + task_status__in=[PENDING, IN_PROGRESS], + due_date__range=[datetime.datetime.now().date(), + (datetime.datetime.now().date() + datetime.timedelta(days=1))]) + if pending_tasks := all_pending_tasks.filter(task_status=PENDING): + for task in pending_tasks: + send_notification(PENDING_TASK_EXPIRING, None, None, task.junior.auth.id, + {'task_id': task.id}) + if in_progress_tasks := all_pending_tasks.filter(task_status=IN_PROGRESS): + for task in in_progress_tasks: + send_notification(IN_PROGRESS_TASK_EXPIRING, task.junior.auth.id, JUNIOR, task.guardian.user.id, + {'task_id': task.id}) + return True + + +@shared_task() +def notify_top_junior(): + """ + task to send notification for top leaderboard junior to all junior's + :return: + """ + junior_points_qs = JuniorPoints.objects.filter( + junior__is_verified=True + ).select_related( + 'junior', 'junior__auth' + ).annotate(rank=Window( + expression=Rank(), + order_by=[F('total_points').desc(), 'junior__created_at']) + ).order_by('-total_points', 'junior__created_at') + + prev_top_position = junior_points_qs.filter(position=1).first() + new_top_position = junior_points_qs.filter(rank=1).first() + if prev_top_position != new_top_position: + send_notification_multiple_user(TOP_JUNIOR, new_top_position.junior.auth.id, JUNIOR, + {'points': new_top_position.total_points}) + for junior_point in junior_points_qs: + junior_point.position = junior_point.rank + junior_point.save() + return True diff --git a/celerybeat-schedule b/celerybeat-schedule index f2510fc..f457bb5 100644 Binary files a/celerybeat-schedule and b/celerybeat-schedule differ diff --git a/guardian/migrations/0021_guardian_is_deleted.py b/guardian/migrations/0021_guardian_is_deleted.py new file mode 100644 index 0000000..11833c6 --- /dev/null +++ b/guardian/migrations/0021_guardian_is_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-08-17 12:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('guardian', '0020_alter_juniortask_task_status'), + ] + + operations = [ + migrations.AddField( + model_name='guardian', + name='is_deleted', + field=models.BooleanField(default=False), + ), + ] diff --git a/guardian/models.py b/guardian/models.py index 45afee7..5c6457a 100644 --- a/guardian/models.py +++ b/guardian/models.py @@ -57,6 +57,8 @@ class Guardian(models.Model): is_invited = models.BooleanField(default=False) # Profile activity""" is_password_set = models.BooleanField(default=True) + # guardian profile deleted or not""" + is_deleted = models.BooleanField(default=False) """Profile activity""" is_active = models.BooleanField(default=True) """guardian is verified or not""" diff --git a/guardian/serializers.py b/guardian/serializers.py index 1dabc8d..4206d7a 100644 --- a/guardian/serializers.py +++ b/guardian/serializers.py @@ -1,6 +1,7 @@ """Serializer of Guardian""" # third party imports import logging +from django.contrib.auth import password_validation from rest_framework import serializers # Import Refresh token of jwt from rest_framework_simplejwt.tokens import RefreshToken @@ -23,17 +24,19 @@ from account.models import UserProfile, UserEmailOtp, UserNotification from account.utils import generate_code from junior.serializers import JuniorDetailSerializer from base.messages import ERROR_CODE, SUCCESS_CODE -from base.constants import NUMBER, JUN, ZOD, GRD, Already_register_user +from base.constants import NUMBER, JUN, ZOD, GRD, Already_register_user, GUARDIAN from junior.models import Junior, JuniorPoints, JuniorGuardianRelationship from .utils import real_time, convert_timedelta_into_datetime, update_referral_points # notification's constant -from notifications.constants import TASK_POINTS, TASK_REJECTED +from notifications.constants import TASK_APPROVED, TASK_REJECTED # send notification function from notifications.utils import send_notification - +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ # In this serializer file # define user serializer, +# define password validation # create guardian serializer, # task serializer, # guardian serializer, @@ -42,10 +45,51 @@ from notifications.utils import send_notification # guardian profile serializer, # approve junior serializer, # approve task serializer, +from rest_framework import serializers + +class PasswordValidator: + """Password validation""" + def __init__(self, min_length=8, max_length=None, require_uppercase=True, require_numbers=True): + self.min_length = min_length + self.max_length = max_length + self.require_uppercase = require_uppercase + self.require_numbers = require_numbers + + def __call__(self, value): + self.enforce_password_policy(value) + + def enforce_password_policy(self, password): + # add validation for password + special_characters = "!@#$%^&*()_-+=<>?/[]{}|" + if len(password) < self.min_length: + raise serializers.ValidationError( + _("Password must be at least %(min_length)d characters long.") % {'min_length': self.min_length} + ) + + if self.max_length is not None and len(password) > self.max_length: + # must be 8 character + raise serializers.ValidationError( + _("Password must be at most %(max_length)d characters long.") % {'max_length': self.max_length} + ) + + if self.require_uppercase and not any(char.isupper() for char in password): + # must contain upper case letter + raise serializers.ValidationError(_("Password must contain at least one uppercase letter.")) + + if self.require_numbers and not any(char.isdigit() for char in password): + # must contain digit + raise serializers.ValidationError(_("Password must contain at least one digit.")) + if self.require_numbers and not any(char in special_characters for char in password): + # must contain special character + raise serializers.ValidationError(_("Password must contain at least one special character.")) + + + class UserSerializer(serializers.ModelSerializer): """User serializer""" auth_token = serializers.SerializerMethodField('get_auth_token') + password = serializers.CharField(write_only=True, validators=[PasswordValidator()]) class Meta(object): """Meta info""" @@ -214,7 +258,7 @@ class GuardianDetailSerializer(serializers.ModelSerializer): """Meta info""" model = Guardian fields = ['id', 'email', 'first_name', 'last_name', 'country_code', 'phone', 'gender', 'dob', - 'guardian_code','is_active', 'is_complete_profile', 'created_at', 'image', + 'guardian_code','is_active', 'is_complete_profile', 'created_at', 'image', 'is_deleted', 'updated_at'] class TaskDetailsSerializer(serializers.ModelSerializer): """Task detail serializer""" @@ -279,10 +323,11 @@ class TaskDetailsjuniorSerializer(serializers.ModelSerializer): 'requested_on', 'rejected_on', 'completed_on', 'junior', 'task_status', 'is_active', 'remaining_time', 'created_at','updated_at'] + class TopJuniorSerializer(serializers.ModelSerializer): """Top junior serializer""" junior = JuniorDetailSerializer() - position = serializers.IntegerField() + position = serializers.SerializerMethodField() class Meta(object): """Meta info""" @@ -292,9 +337,13 @@ class TopJuniorSerializer(serializers.ModelSerializer): def to_representation(self, instance): """Convert instance to representation""" representation = super().to_representation(instance) - representation['position'] = instance.position return representation + @staticmethod + def get_position(obj): + """ get position/rank """ + return obj.rank + class GuardianProfileSerializer(serializers.ModelSerializer): """junior serializer""" @@ -340,7 +389,7 @@ class GuardianProfileSerializer(serializers.ModelSerializer): fields = ['id', 'email', 'first_name', 'last_name', 'country_name','country_code', 'phone', 'gender', 'dob', 'guardian_code', 'notification_count', 'total_count', 'complete_field_count', 'referral_code', 'is_active', 'is_complete_profile', 'created_at', 'image', 'signup_method', - 'updated_at', 'passcode'] + 'updated_at', 'passcode','is_deleted'] class ApproveJuniorSerializer(serializers.ModelSerializer): @@ -353,9 +402,9 @@ class ApproveJuniorSerializer(serializers.ModelSerializer): def create(self, validated_data): """update guardian code""" instance = self.context['junior'] - instance.guardian_code = [self.context['guardian_code']] - instance.guardian_code_approved = True - instance.guardian_code_status = str(NUMBER['two']) + guardian_code = self.context['guardian_code'] + index = instance.guardian_code.index(guardian_code) + instance.guardian_code_status[index] = str(NUMBER['two']) instance.save() return instance @@ -383,7 +432,8 @@ class ApproveTaskSerializer(serializers.ModelSerializer): # update complete time of task # instance.completed_on = real_time() instance.completed_on = timezone.now().astimezone(pytz.utc) - send_notification.delay(TASK_POINTS, None, junior_details.auth.id, {}) + send_notification.delay(TASK_APPROVED, instance.guardian.user.id, GUARDIAN, + junior_details.auth.id, {'task_id': instance.id}) else: # reject the task instance.task_status = str(NUMBER['three']) @@ -391,7 +441,8 @@ class ApproveTaskSerializer(serializers.ModelSerializer): # update reject time of task # instance.rejected_on = real_time() instance.rejected_on = timezone.now().astimezone(pytz.utc) - send_notification.delay(TASK_REJECTED, None, junior_details.auth.id, {}) + send_notification.delay(TASK_REJECTED, instance.guardian.user.id, GUARDIAN, + junior_details.auth.id, {'task_id': instance.id}) instance.save() junior_data.save() return junior_details @@ -461,4 +512,7 @@ class GuardianDetailListSerializer(serializers.ModelSerializer): def get_guardian_code_status(self,obj): """guardian code status""" - return obj.junior.guardian_code_status + if obj.guardian.guardian_code in obj.junior.guardian_code: + index = obj.junior.guardian_code.index(obj.guardian.guardian_code) + data = obj.junior.guardian_code_status[index] + return data diff --git a/guardian/tasks.py b/guardian/tasks.py index 9cf39b3..f40d232 100644 --- a/guardian/tasks.py +++ b/guardian/tasks.py @@ -1,7 +1,12 @@ """task files""" -"""Django import""" + +# Django import import secrets + + def generate_otp(): - """generate random otp""" + """ + generate random otp + """ digits = "0123456789" return "".join(secrets.choice(digits) for _ in range(6)) diff --git a/guardian/urls.py b/guardian/urls.py index e95ea8e..4a1d006 100644 --- a/guardian/urls.py +++ b/guardian/urls.py @@ -1,7 +1,7 @@ """ Urls files""" """Django import""" from django.urls import path, include -from .views import (SignupViewset, UpdateGuardianProfile, AllTaskListAPIView, CreateTaskAPIView, TaskListAPIView, +from .views import (SignupViewset, UpdateGuardianProfile, CreateTaskAPIView, TaskListAPIView, SearchTaskListAPIView, TopJuniorListAPIView, ApproveJuniorAPIView, ApproveTaskAPIView, GuardianListAPIView) """Third party import""" @@ -25,8 +25,6 @@ router.register('sign-up', SignupViewset, basename='sign-up') router.register('create-guardian-profile', UpdateGuardianProfile, basename='update-guardian-profile') # Create Task API""" router.register('create-task', CreateTaskAPIView, basename='create-task') -# All Task list API""" -router.register('all-task-list', AllTaskListAPIView, basename='all-task-list') # Task list bases on the status API""" router.register('task-list', TaskListAPIView, basename='task-list') # Leaderboard API""" diff --git a/guardian/utils.py b/guardian/utils.py index 57e8080..1d40c34 100644 --- a/guardian/utils.py +++ b/guardian/utils.py @@ -22,6 +22,8 @@ from zod_bank.celery import app from notifications.constants import REFERRAL_POINTS # send notification function from notifications.utils import send_notification + + # Define upload image on # ali baba cloud # firstly save image @@ -41,18 +43,41 @@ def upload_image_to_alibaba(image, filename): # Save the image object to a temporary file with tempfile.NamedTemporaryFile(delete=False) as temp_file: """write image in temporary file""" - if type(image) == bytes: - temp_file.write(image) - else: - temp_file.write(image.read()) - """auth of bucket""" - auth = oss2.Auth(settings.ALIYUN_OSS_ACCESS_KEY_ID, settings.ALIYUN_OSS_ACCESS_KEY_SECRET) - """fetch bucket details""" - bucket = oss2.Bucket(auth, settings.ALIYUN_OSS_ENDPOINT, settings.ALIYUN_OSS_BUCKET_NAME) - # Upload the temporary file to Alibaba OSS - bucket.put_object_from_file(filename, temp_file.name) - """create perfect url for image""" - new_filename = filename.replace(' ', '%20') + temp_file.write(image.read()) + return upload_file_to_alibaba(temp_file, filename) + + +def upload_base64_image_to_alibaba(image, filename): + """ + upload image on oss alibaba bucket + """ + # Save the image object to a temporary file + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + # write image in temporary file + temp_file.write(image) + return upload_file_to_alibaba(temp_file, filename) + + +def upload_excel_file_to_alibaba(response, filename): + """ + upload excel file on oss alibaba bucket + """ + # Save the image object to a temporary file + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + # write image in temporary file + temp_file.write(response.content) + return upload_file_to_alibaba(temp_file, filename) + + +def upload_file_to_alibaba(temp_file, filename): + """auth of bucket""" + auth = oss2.Auth(settings.ALIYUN_OSS_ACCESS_KEY_ID, settings.ALIYUN_OSS_ACCESS_KEY_SECRET) + """fetch bucket details""" + bucket = oss2.Bucket(auth, settings.ALIYUN_OSS_ENDPOINT, settings.ALIYUN_OSS_BUCKET_NAME) + # Upload the temporary file to Alibaba OSS + bucket.put_object_from_file(filename, temp_file.name) + """create perfect url for image""" + new_filename = filename.replace(' ', '%20') return f"https://{settings.ALIYUN_OSS_BUCKET_NAME}.{settings.ALIYUN_OSS_ENDPOINT}/{new_filename}" @@ -92,7 +117,7 @@ def update_referral_points(referral_code, referral_code_used): junior_query.total_points = junior_query.total_points + NUMBER['five'] junior_query.referral_points = junior_query.referral_points + NUMBER['five'] junior_query.save() - send_notification.delay(REFERRAL_POINTS, None, junior_queryset.auth.id, {}) + send_notification.delay(REFERRAL_POINTS, None, None, junior_queryset.auth.id, {}) @@ -102,7 +127,7 @@ def update_expired_task_status(data=None): Update task of the status if due date is in past """ try: - task_status = [str(NUMBER['one']), str(NUMBER['two']), str(NUMBER['four'])] + task_status = [str(NUMBER['one']), str(NUMBER['two'])] JuniorTask.objects.filter(due_date__lt=datetime.today().date(), task_status__in=task_status).update(task_status=str(NUMBER['six'])) except ObjectDoesNotExist as e: diff --git a/guardian/views.py b/guardian/views.py index 3948e6f..e120681 100644 --- a/guardian/views.py +++ b/guardian/views.py @@ -6,11 +6,13 @@ # Import PageNumberPagination # Import User # Import timezone +from django.db.models import F, Window +from django.db.models.functions.window import Rank from rest_framework.permissions import IsAuthenticated from rest_framework import viewsets, status from rest_framework.pagination import PageNumberPagination from django.contrib.auth.models import User - +from base.constants import guardian_code_tuple from rest_framework.filters import SearchFilter from django.utils import timezone @@ -32,13 +34,13 @@ from .serializers import (UserSerializer, CreateGuardianSerializer, TaskSerializ GuardianDetailListSerializer) from .models import Guardian, JuniorTask from junior.models import Junior, JuniorPoints, JuniorGuardianRelationship -from account.models import UserEmailOtp, UserNotification +from account.models import UserEmailOtp, UserNotification, UserDeviceDetails from .tasks import generate_otp from account.utils import custom_response, custom_error_response, OTP_EXPIRY, send_otp_email from base.messages import ERROR_CODE, SUCCESS_CODE -from base.constants import NUMBER, GUARDIAN_CODE_STATUS +from base.constants import NUMBER, GUARDIAN_CODE_STATUS, GUARDIAN from .utils import upload_image_to_alibaba -from notifications.constants import REGISTRATION, TASK_CREATED, LEADERBOARD_RANKING +from notifications.constants import REGISTRATION, TASK_ASSIGNED, ASSOCIATE_APPROVED, ASSOCIATE_REJECTED from notifications.utils import send_notification """ Define APIs """ @@ -57,8 +59,10 @@ class SignupViewset(viewsets.ModelViewSet): """Signup view set""" queryset = User.objects.all() serializer_class = UserSerializer + http_method_names = ('post',) def create(self, request, *args, **kwargs): """Create user profile""" + device_id = request.META.get('HTTP_DEVICE_ID') if request.data['user_type'] in [str(NUMBER['one']), str(NUMBER['two'])]: serializer = UserSerializer(context=request.data['user_type'], data=request.data) if serializer.is_valid(): @@ -66,24 +70,25 @@ class SignupViewset(viewsets.ModelViewSet): """Generate otp""" otp = generate_otp() # expire otp after 1 day - expiry = OTP_EXPIRY + expiry = timezone.now() + timezone.timedelta(days=1) # create user email otp object UserEmailOtp.objects.create(email=request.data['email'], otp=otp, user_type=str(request.data['user_type']), expired_at=expiry) """Send email to the register user""" - send_otp_email(request.data['email'], otp) - # send push notification for registration - send_notification.delay(REGISTRATION, None, user.id, {}) + send_otp_email.delay(request.data['email'], otp) + UserDeviceDetails.objects.create(user=user, device_id=device_id) return custom_response(SUCCESS_CODE['3001'], response_status=status.HTTP_200_OK) return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST) else: return custom_error_response(ERROR_CODE['2028'], response_status=status.HTTP_400_BAD_REQUEST) -class UpdateGuardianProfile(viewsets.ViewSet): + +class UpdateGuardianProfile(viewsets.ModelViewSet): """Update guardian profile""" serializer_class = CreateGuardianSerializer permission_classes = [IsAuthenticated] + http_method_names = ('post',) def create(self, request, *args, **kwargs): """Create guardian profile""" @@ -126,7 +131,11 @@ class AllTaskListAPIView(viewsets.ModelViewSet): class TaskListAPIView(viewsets.ModelViewSet): - """Update guardian profile""" + """Task list + Params + status + search + page""" serializer_class = TaskDetailsSerializer permission_classes = [IsAuthenticated] filter_backends = (SearchFilter,) @@ -163,42 +172,56 @@ class CreateTaskAPIView(viewsets.ModelViewSet): http_method_names = ('post', ) def create(self, request, *args, **kwargs): + """ + image should be in form data + """ try: image = request.data['default_image'] junior = request.data['junior'] - allowed_extensions = ['.jpg', '.jpeg', '.png'] - if not any(extension in str(image) for extension in allowed_extensions): - return custom_error_response(ERROR_CODE['2048'], response_status=status.HTTP_400_BAD_REQUEST) - if not junior.isnumeric(): - """junior value must be integer""" - return custom_error_response(ERROR_CODE['2047'], response_status=status.HTTP_400_BAD_REQUEST) - data = request.data - if 'https' in str(image): - image_data = image + junior_id = Junior.objects.filter(id=junior).last() + if junior_id: + guardian_data = Guardian.objects.filter(user=request.user).last() + index = junior_id.guardian_code.index(guardian_data.guardian_code) + status_index = junior_id.guardian_code_status[index] + if status_index == str(NUMBER['three']): + return custom_error_response(ERROR_CODE['2078'], response_status=status.HTTP_400_BAD_REQUEST) + allowed_extensions = ['.jpg', '.jpeg', '.png'] + if not any(extension in str(image) for extension in allowed_extensions): + return custom_error_response(ERROR_CODE['2048'], response_status=status.HTTP_400_BAD_REQUEST) + if not junior.isnumeric(): + """junior value must be integer""" + return custom_error_response(ERROR_CODE['2047'], response_status=status.HTTP_400_BAD_REQUEST) + data = request.data + if 'https' in str(image): + image_data = image + else: + filename = f"images/{image}" + if image and image.size == NUMBER['zero']: + return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST) + image_url = upload_image_to_alibaba(image, filename) + image_data = image_url + data.pop('default_image') + # use TaskSerializer serializer + serializer = TaskSerializer(context={"user":request.user, "image":image_data}, data=data) + if serializer.is_valid(): + # save serializer + task = serializer.save() + + send_notification.delay(TASK_ASSIGNED, request.auth.payload['user_id'], GUARDIAN, + junior_id.auth.id, {'task_id': task.id}) + return custom_response(SUCCESS_CODE['3018'], serializer.data, response_status=status.HTTP_200_OK) + return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST) else: - filename = f"images/{image}" - if image and image.size == NUMBER['zero']: - return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST) - image_url = upload_image_to_alibaba(image, filename) - image_data = image_url - data.pop('default_image') - # use TaskSerializer serializer - serializer = TaskSerializer(context={"user":request.user, "image":image_data}, data=data) - if serializer.is_valid(): - # save serializer - serializer.save() - junior_id = Junior.objects.filter(id=junior).last() - send_notification.delay(TASK_CREATED, None, junior_id.auth.id, {}) - return custom_response(SUCCESS_CODE['3018'], serializer.data, response_status=status.HTTP_200_OK) - return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST) + return custom_error_response(ERROR_CODE['2047'], response_status=status.HTTP_400_BAD_REQUEST) except Exception as e: return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) class SearchTaskListAPIView(viewsets.ModelViewSet): - """Update guardian profile""" + """Filter task""" serializer_class = TaskDetailsSerializer permission_classes = [IsAuthenticated] pagination_class = PageNumberPagination + http_method_names = ('get',) def get_queryset(self): """Get the queryset for the view""" @@ -209,7 +232,7 @@ class SearchTaskListAPIView(viewsets.ModelViewSet): return junior_queryset def list(self, request, *args, **kwargs): - """Create guardian profile""" + """Filter task""" try: queryset = self.get_queryset() paginator = self.pagination_class() @@ -223,10 +246,11 @@ class SearchTaskListAPIView(viewsets.ModelViewSet): class TopJuniorListAPIView(viewsets.ModelViewSet): - """Top juniors list""" - queryset = JuniorPoints.objects.all() + """Top juniors list + No Params""" serializer_class = TopJuniorSerializer permission_classes = [IsAuthenticated] + http_method_names = ('get',) def get_serializer_context(self): # context list @@ -234,97 +258,116 @@ class TopJuniorListAPIView(viewsets.ModelViewSet): context.update({'view': self}) return context + def get_queryset(self): + queryset = JuniorPoints.objects.filter( + junior__is_verified=True + ).select_related( + 'junior', 'junior__auth' + ).annotate(rank=Window( + expression=Rank(), + order_by=[F('total_points').desc(), 'junior__created_at']) + ).order_by('-total_points', 'junior__created_at') + return queryset + def list(self, request, *args, **kwargs): """Fetch junior list of those who complete their tasks""" try: - junior_total_points = self.get_queryset().order_by('-total_points') - # Update the position field for each JuniorPoints object - for index, junior in enumerate(junior_total_points): - junior.position = index + 1 - send_notification.delay(LEADERBOARD_RANKING, None, junior.junior.auth.id, {}) - junior.save() - serializer = self.get_serializer(junior_total_points[:NUMBER['fifteen']], many=True) + junior_total_points = self.get_queryset()[:15] + serializer = self.get_serializer(junior_total_points, many=True) return custom_response(None, serializer.data, response_status=status.HTTP_200_OK) except Exception as e: return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) -class ApproveJuniorAPIView(viewsets.ViewSet): +class ApproveJuniorAPIView(viewsets.ModelViewSet): """approve junior by guardian""" serializer_class = ApproveJuniorSerializer permission_classes = [IsAuthenticated] - - def get_queryset(self): - """Get the queryset for the view""" - guardian = Guardian.objects.filter(user__email=self.request.user).last() - # fetch junior query - junior_queryset = Junior.objects.filter(id=self.request.data.get('junior_id')).last() - return guardian, junior_queryset + http_method_names = ('post',) def create(self, request, *args, **kwargs): - """ junior list""" + """ Use below param + {"junior_id":"75", + "action":"1"} + """ try: - queryset = self.get_queryset() + guardian = Guardian.objects.filter(user__email=self.request.user).last() + # fetch junior query + junior_queryset = Junior.objects.filter(id=self.request.data.get('junior_id')).last() + if junior_queryset and (junior_queryset.is_deleted or not junior_queryset.is_active): + return custom_error_response(ERROR_CODE['2073'], response_status=status.HTTP_400_BAD_REQUEST) # action 1 is use for approve and 2 for reject if request.data['action'] == '1': # use ApproveJuniorSerializer serializer - serializer = ApproveJuniorSerializer(context={"guardian_code": queryset[0].guardian_code, - "junior": queryset[1], "action": request.data['action']}, + serializer = ApproveJuniorSerializer(context={"guardian_code": guardian.guardian_code, + "junior": junior_queryset, + "action": request.data['action']}, data=request.data) if serializer.is_valid(): # save serializer serializer.save() + send_notification.delay(ASSOCIATE_APPROVED, guardian.user.id, GUARDIAN, + junior_queryset.auth.id, {}) return custom_response(SUCCESS_CODE['3023'], serializer.data, response_status=status.HTTP_200_OK) else: - queryset[1].guardian_code = None - queryset[1].guardian_code_status = str(NUMBER['one']) - queryset[1].save() + if junior_queryset.guardian_code and ('-' in junior_queryset.guardian_code): + junior_queryset.guardian_code.remove('-') + if junior_queryset.guardian_code_status and ('-' in junior_queryset.guardian_code_status): + junior_queryset.guardian_code_status.remove('-') + index = junior_queryset.guardian_code.index(guardian.guardian_code) + junior_queryset.guardian_code.remove(guardian.guardian_code) + junior_queryset.guardian_code_status.pop(index) + junior_queryset.save() + send_notification.delay(ASSOCIATE_REJECTED, guardian.user.id, GUARDIAN, junior_queryset.auth.id, {}) return custom_response(SUCCESS_CODE['3024'], response_status=status.HTTP_200_OK) except Exception as e: return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) -class ApproveTaskAPIView(viewsets.ViewSet): +class ApproveTaskAPIView(viewsets.ModelViewSet): """approve junior by guardian""" serializer_class = ApproveTaskSerializer permission_classes = [IsAuthenticated] - - def get_queryset(self): - """Get the queryset for the view""" - guardian = Guardian.objects.filter(user__email=self.request.user).last() - # task query - task_queryset = JuniorTask.objects.filter(id=self.request.data.get('task_id'), - guardian=guardian, - junior=self.request.data.get('junior_id')).last() - return guardian, task_queryset - + http_method_names = ('post',) def create(self, request, *args, **kwargs): - """ junior list""" + """ Params + {"junior_id":"82", + "task_id":"43", + "action":"1"} + action 1 for approve + 2 for reject + """ # action 1 is use for approve and 2 for reject try: - queryset = self.get_queryset() + guardian = Guardian.objects.filter(user__email=self.request.user).last() + # task query + task_queryset = JuniorTask.objects.filter(id=self.request.data.get('task_id'), + guardian=guardian, + junior=self.request.data.get('junior_id')).last() + if task_queryset and (task_queryset.junior.is_deleted or not task_queryset.junior.is_active): + return custom_error_response(ERROR_CODE['2072'], response_status=status.HTTP_400_BAD_REQUEST) # use ApproveJuniorSerializer serializer - serializer = ApproveTaskSerializer(context={"guardian_code": queryset[0].guardian_code, - "task_instance": queryset[1], + serializer = ApproveTaskSerializer(context={"guardian_code": guardian.guardian_code, + "task_instance": task_queryset, "action": str(request.data['action']), "junior": self.request.data['junior_id']}, data=request.data) unexpected_task_status = [str(NUMBER['five']), str(NUMBER['six'])] if (str(request.data['action']) == str(NUMBER['one']) and serializer.is_valid() - and queryset[1] and queryset[1].task_status not in unexpected_task_status): + and task_queryset and task_queryset.task_status not in unexpected_task_status): # save serializer serializer.save() return custom_response(SUCCESS_CODE['3025'], response_status=status.HTTP_200_OK) elif (str(request.data['action']) == str(NUMBER['two']) and serializer.is_valid() - and queryset[1] and queryset[1].task_status not in unexpected_task_status): + and task_queryset and task_queryset.task_status not in unexpected_task_status): # save serializer serializer.save() return custom_response(SUCCESS_CODE['3026'], response_status=status.HTTP_200_OK) else: - return custom_response(ERROR_CODE['2038'], response_status=status.HTTP_400_BAD_REQUEST) + return custom_error_response(ERROR_CODE['2038'], response_status=status.HTTP_400_BAD_REQUEST) except Exception as e: return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) - +# class GuardianListAPIView(viewsets.ModelViewSet): """Guardian list of assosicated junior""" @@ -333,7 +376,8 @@ class GuardianListAPIView(viewsets.ModelViewSet): http_method_names = ('get',) def list(self, request, *args, **kwargs): - """ junior list""" + """ Guardian list of assosicated junior + No Params""" try: guardian_data = JuniorGuardianRelationship.objects.filter(junior__auth__email=self.request.user) # fetch junior object diff --git a/junior/admin.py b/junior/admin.py index 6c6cdf9..2ffda51 100644 --- a/junior/admin.py +++ b/junior/admin.py @@ -3,8 +3,9 @@ from django.contrib import admin """Import Django app""" from .models import (Junior, JuniorPoints, JuniorGuardianRelationship, JuniorArticlePoints, JuniorArticle, - JuniorArticleCard) + JuniorArticleCard, FAQ) # Register your models here. +admin.site.register(FAQ) @admin.register(JuniorArticle) class JuniorArticleAdmin(admin.ModelAdmin): """Junior Admin""" diff --git a/junior/migrations/0026_faq_alter_juniorarticle_options_and_more.py b/junior/migrations/0026_faq_alter_juniorarticle_options_and_more.py new file mode 100644 index 0000000..12243a2 --- /dev/null +++ b/junior/migrations/0026_faq_alter_juniorarticle_options_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.2 on 2023-08-17 09:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('junior', '0025_alter_juniorarticle_junior'), + ] + + operations = [ + migrations.CreateModel( + name='FAQ', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question', models.IntegerField(max_length=100)), + ('description', models.CharField(max_length=500)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'FAQ', + 'verbose_name_plural': 'FAQ', + }, + ), + migrations.AlterModelOptions( + name='juniorarticle', + options={'verbose_name': 'Junior Article', 'verbose_name_plural': 'Junior Article'}, + ), + migrations.AlterModelOptions( + name='juniorarticlecard', + options={'verbose_name': 'Junior Article Card', 'verbose_name_plural': 'Junior Article Card'}, + ), + migrations.AlterModelOptions( + name='juniorarticlepoints', + options={'verbose_name': 'Junior Article Points', 'verbose_name_plural': 'Junior Article Points'}, + ), + ] diff --git a/junior/migrations/0027_alter_faq_question.py b/junior/migrations/0027_alter_faq_question.py new file mode 100644 index 0000000..46fefd8 --- /dev/null +++ b/junior/migrations/0027_alter_faq_question.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-08-17 09:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('junior', '0026_faq_alter_juniorarticle_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='faq', + name='question', + field=models.CharField(max_length=100), + ), + ] diff --git a/junior/migrations/0028_faq_status.py b/junior/migrations/0028_faq_status.py new file mode 100644 index 0000000..bdfbf67 --- /dev/null +++ b/junior/migrations/0028_faq_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-08-17 10:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('junior', '0027_alter_faq_question'), + ] + + operations = [ + migrations.AddField( + model_name='faq', + name='status', + field=models.IntegerField(blank=True, default=1, null=True), + ), + ] diff --git a/junior/migrations/0029_junior_is_deleted.py b/junior/migrations/0029_junior_is_deleted.py new file mode 100644 index 0000000..a39f60f --- /dev/null +++ b/junior/migrations/0029_junior_is_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-08-17 12:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('junior', '0028_faq_status'), + ] + + operations = [ + migrations.AddField( + model_name='junior', + name='is_deleted', + field=models.BooleanField(default=False), + ), + ] diff --git a/junior/migrations/0030_remove_junior_guardian_code_status.py b/junior/migrations/0030_remove_junior_guardian_code_status.py new file mode 100644 index 0000000..6949e9a --- /dev/null +++ b/junior/migrations/0030_remove_junior_guardian_code_status.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.2 on 2023-08-26 08:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('junior', '0029_junior_is_deleted'), + ] + + operations = [ + migrations.RemoveField( + model_name='junior', + name='guardian_code_status', + ), + ] diff --git a/junior/migrations/0031_junior_guardian_code_status.py b/junior/migrations/0031_junior_guardian_code_status.py new file mode 100644 index 0000000..c342f28 --- /dev/null +++ b/junior/migrations/0031_junior_guardian_code_status.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.2 on 2023-08-26 08:59 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('junior', '0030_remove_junior_guardian_code_status'), + ] + + operations = [ + migrations.AddField( + model_name='junior', + name='guardian_code_status', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, default=None, max_length=10, null=True), null=True, size=None), + ), + ] diff --git a/junior/models.py b/junior/models.py index 025843e..559acfe 100644 --- a/junior/models.py +++ b/junior/models.py @@ -68,15 +68,17 @@ class Junior(models.Model): is_password_set = models.BooleanField(default=True) # junior profile is complete or not""" is_complete_profile = models.BooleanField(default=False) + # junior profile deleted or not""" + is_deleted = models.BooleanField(default=False) # passcode of the junior profile""" passcode = models.IntegerField(null=True, blank=True, default=None) # junior is verified or not""" is_verified = models.BooleanField(default=False) """guardian code is approved or not""" guardian_code_approved = models.BooleanField(default=False) - # guardian code status""" - guardian_code_status = models.CharField(max_length=31, choices=GUARDIAN_CODE_STATUS, default='1', - null=True, blank=True) + # # guardian code status""" + guardian_code_status = ArrayField(models.CharField(max_length=10, null=True, blank=True, default=None), null=True, + ) # Profile created and updated time""" created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -158,6 +160,12 @@ class JuniorArticlePoints(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + class Meta(object): + """ Meta class """ + verbose_name = 'Junior Article Points' + # another name of the model""" + verbose_name_plural = 'Junior Article Points' + def __str__(self): """Return title""" return f'{self.id} | {self.question}' @@ -178,6 +186,12 @@ class JuniorArticle(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + class Meta(object): + """ Meta class """ + verbose_name = 'Junior Article' + # another name of the model""" + verbose_name_plural = 'Junior Article' + def __str__(self): """Return title""" return f'{self.id} | {self.article}' @@ -197,6 +211,34 @@ class JuniorArticleCard(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + class Meta(object): + """ Meta class """ + verbose_name = 'Junior Article Card' + # another name of the model""" + verbose_name_plural = 'Junior Article Card' + def __str__(self): """Return title""" return f'{self.id} | {self.article}' + + +class FAQ(models.Model): + """FAQ model""" + # questions""" + question = models.CharField(max_length=100) + # answer""" + description = models.CharField(max_length=500) + # status + status = models.IntegerField(default=1, null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta(object): + """ Meta class """ + verbose_name = 'FAQ' + # another name of the model""" + verbose_name_plural = 'FAQ' + + def __str__(self): + """Return email id""" + return f'{self.question}' diff --git a/junior/serializers.py b/junior/serializers.py index c3e2d72..de5f671 100644 --- a/junior/serializers.py +++ b/junior/serializers.py @@ -12,19 +12,19 @@ from rest_framework_simplejwt.tokens import RefreshToken # local imports from account.utils import send_otp_email, generate_code -from junior.models import Junior, JuniorPoints, JuniorGuardianRelationship, JuniorArticlePoints +from junior.models import Junior, JuniorPoints, JuniorGuardianRelationship, JuniorArticlePoints, FAQ from guardian.tasks import generate_otp from base.messages import ERROR_CODE, SUCCESS_CODE from base.constants import (PENDING, IN_PROGRESS, REJECTED, REQUESTED, COMPLETED, NUMBER, JUN, ZOD, EXPIRED, - GUARDIAN_CODE_STATUS) + GUARDIAN_CODE_STATUS, JUNIOR) from guardian.models import Guardian, JuniorTask from account.models import UserEmailOtp, UserNotification -from junior.utils import junior_notification_email, junior_approval_mail +from junior.utils import junior_notification_email, junior_approval_mail, get_junior_leaderboard_rank from guardian.utils import real_time, update_referral_points, convert_timedelta_into_datetime from notifications.utils import send_notification -from notifications.constants import (INVITED_GUARDIAN, APPROVED_JUNIOR, SKIPPED_PROFILE_SETUP, TASK_ACTION, - TASK_SUBMITTED) - +from notifications.constants import (ASSOCIATE_REQUEST, ASSOCIATE_JUNIOR, TASK_ACTION, + ) +from web_admin.models import ArticleCard class ListCharField(serializers.ListField): """Serializer for Array field""" @@ -88,17 +88,27 @@ class CreateJuniorSerializer(serializers.ModelSerializer): if junior: """update details according to the data get from request""" junior.gender = validated_data.get('gender',junior.gender) - """Update guardian code""" - junior.guardian_code = validated_data.get('guardian_code', junior.guardian_code) - """condition for guardian code""" + # Update guardian code""" + # condition for guardian code if guardian_code: - junior.guardian_code = guardian_code + if junior.guardian_code and guardian_code: + if guardian_code[0] in junior.guardian_code: + raise serializers.ValidationError({"error":ERROR_CODE['2076'],"code":"400", "status":"failed"}) + if not junior.guardian_code: + junior.guardian_code = [] + junior.guardian_code_status = [] + junior.guardian_code.extend(guardian_code) + junior.guardian_code_status.extend(str(NUMBER['three'])) + elif len(junior.guardian_code) < 3 and len(guardian_code) < 3: + junior.guardian_code.extend(guardian_code) + junior.guardian_code_status.extend(str(NUMBER['three'])) + else: + raise serializers.ValidationError({"error":ERROR_CODE['2081'],"code":"400", "status":"failed"}) guardian_data = Guardian.objects.filter(guardian_code=guardian_code[0]).last() if guardian_data: JuniorGuardianRelationship.objects.get_or_create(guardian=guardian_data, junior=junior) - junior.guardian_code_status = str(NUMBER['three']) - junior_approval_mail(user.email, user.first_name) - send_notification.delay(APPROVED_JUNIOR, None, guardian_data.user.id, {}) + send_notification.delay(ASSOCIATE_REQUEST, junior.auth.id, JUNIOR, guardian_data.user.id, {}) + junior_approval_mail.delay(user.email, user.first_name) junior.dob = validated_data.get('dob', junior.dob) junior.passcode = validated_data.get('passcode', junior.passcode) junior.country_name = validated_data.get('country_name', junior.country_name) @@ -147,7 +157,7 @@ class JuniorDetailSerializer(serializers.ModelSerializer): model = Junior fields = ['id', 'email', 'first_name', 'last_name', 'country_code', 'phone', 'gender', 'dob', 'guardian_code', 'image', 'is_invited', 'referral_code','is_active', 'is_complete_profile', - 'created_at', 'image', 'updated_at'] + 'created_at', 'image', 'is_deleted', 'updated_at'] class JuniorDetailListSerializer(serializers.ModelSerializer): """junior serializer""" @@ -163,6 +173,7 @@ class JuniorDetailListSerializer(serializers.ModelSerializer): rejected_task = serializers.SerializerMethodField('get_rejected_task') pending_task = serializers.SerializerMethodField('get_pending_task') position = serializers.SerializerMethodField('get_position') + guardian_code_status = serializers.SerializerMethodField('get_guardian_code_status') def get_auth(self, obj): @@ -179,9 +190,8 @@ class JuniorDetailListSerializer(serializers.ModelSerializer): return data def get_position(self, obj): - data = JuniorPoints.objects.filter(junior=obj).last() - if data: - return data.position + return get_junior_leaderboard_rank(obj) + def get_points(self, obj): data = JuniorPoints.objects.filter(junior=obj).last() if data: @@ -208,13 +218,21 @@ class JuniorDetailListSerializer(serializers.ModelSerializer): def get_pending_task(self, obj): data = JuniorTask.objects.filter(junior=obj, task_status=PENDING).count() return data + + def get_guardian_code_status(self, obj): + if self.context['guardian_code'] in obj.guardian_code: + index = obj.guardian_code.index(self.context['guardian_code']) + if obj.guardian_code_status: + data = obj.guardian_code_status[index] + return data class Meta(object): """Meta info""" model = Junior fields = ['id', 'email', 'first_name', 'last_name', 'country_code', 'country_name', 'phone', 'gender', 'dob', 'guardian_code', 'referral_code','is_active', 'is_complete_profile', 'created_at', 'image', 'updated_at', 'assigned_task','points', 'pending_task', 'in_progress_task', 'completed_task', - 'requested_task', 'rejected_task', 'position', 'is_invited', 'guardian_code_status'] + 'requested_task', 'rejected_task', 'position', 'is_invited', 'guardian_code_status', + 'is_deleted'] class JuniorProfileSerializer(serializers.ModelSerializer): """junior serializer""" @@ -257,7 +275,7 @@ class JuniorProfileSerializer(serializers.ModelSerializer): fields = ['id', 'email', 'first_name', 'last_name', 'country_name', 'country_code', 'phone', 'gender', 'dob', 'guardian_code', 'referral_code','is_active', 'is_complete_profile', 'created_at', 'image', 'updated_at', 'notification_count', 'total_count', 'complete_field_count', 'signup_method', - 'is_invited', 'passcode', 'guardian_code_approved'] + 'is_invited', 'passcode', 'guardian_code_approved', 'is_deleted'] class AddJuniorSerializer(serializers.ModelSerializer): """Add junior serializer""" @@ -290,7 +308,7 @@ class AddJuniorSerializer(serializers.ModelSerializer): referral_code=generate_code(ZOD, user_data.id), referral_code_used=guardian_data.referral_code, is_password_set=False, is_verified=True, - guardian_code_status=GUARDIAN_CODE_STATUS[1][0]) + guardian_code_status=[str(NUMBER['two'])]) JuniorGuardianRelationship.objects.create(guardian=guardian_data, junior=junior_data, relationship=relationship) total_junior = Junior.objects.all().count() @@ -303,9 +321,9 @@ class AddJuniorSerializer(serializers.ModelSerializer): # add push notification UserNotification.objects.get_or_create(user=user_data) """Notification email""" - junior_notification_email(email, full_name, email, password) + junior_notification_email.delay(email, full_name, email, password) # push notification - send_notification.delay(SKIPPED_PROFILE_SETUP, None, junior_data.auth.id, {}) + send_notification.delay(ASSOCIATE_JUNIOR, None, None, junior_data.auth.id, {}) return junior_data @@ -318,9 +336,13 @@ class RemoveJuniorSerializer(serializers.ModelSerializer): fields = ('id', 'is_invited') def update(self, instance, validated_data): if instance: + guardian_code = self.context['guardian_code'] instance.is_invited = False - instance.guardian_code = '{}' - instance.guardian_code_status = str(NUMBER['one']) + if instance.guardian_code and ('-' in instance.guardian_code): + instance.guardian_code.remove('-') + index = instance.guardian_code.index(guardian_code) + instance.guardian_code.remove(guardian_code) + instance.guardian_code_status.pop(index) instance.save() return instance @@ -337,8 +359,8 @@ class CompleteTaskSerializer(serializers.ModelSerializer): instance.task_status = str(NUMBER['four']) instance.is_approved = False instance.save() - send_notification.delay(TASK_SUBMITTED, None, instance.junior.auth.id, {}) - send_notification.delay(TASK_ACTION, None, instance.guardian.user.id, {}) + send_notification.delay(TASK_ACTION, instance.junior.auth.id, JUNIOR, + instance.guardian.user.id, {'task_id': instance.id}) return instance class JuniorPointsSerializer(serializers.ModelSerializer): @@ -358,10 +380,7 @@ class JuniorPointsSerializer(serializers.ModelSerializer): return obj.junior.id def get_position(self, obj): - data = JuniorPoints.objects.filter(junior=obj.junior).last() - if data: - return data.position - return 99999 + return get_junior_leaderboard_rank(obj.junior) def get_points(self, obj): """total points""" points = JuniorPoints.objects.filter(junior=obj.junior).last() @@ -393,7 +412,7 @@ class JuniorPointsSerializer(serializers.ModelSerializer): """Meta info""" model = Junior fields = ['junior_id', 'total_points', 'position', 'pending_task', 'in_progress_task', 'completed_task', - 'requested_task', 'rejected_task', 'expired_task'] + 'requested_task', 'rejected_task', 'expired_task', 'is_deleted'] class AddGuardianSerializer(serializers.ModelSerializer): """Add guardian serializer""" @@ -447,9 +466,8 @@ class AddGuardianSerializer(serializers.ModelSerializer): """Notification email""" junior_notification_email(email, full_name, email, password) - junior_approval_mail(email, full_name) - send_notification.delay(INVITED_GUARDIAN, None, junior_data.auth.id, {}) - send_notification.delay(APPROVED_JUNIOR, None, guardian_data.user.id, {}) + junior_approval_mail.delay(email, full_name) + send_notification.delay(ASSOCIATE_REQUEST, junior_data.auth.id, JUNIOR, guardian_data.user.id, {}) return guardian_data class StartTaskSerializer(serializers.ModelSerializer): @@ -493,8 +511,6 @@ class ReAssignTaskSerializer(serializers.ModelSerializer): instance.save() return instance - - class RemoveGuardianCodeSerializer(serializers.ModelSerializer): """User task Serializer""" class Meta(object): @@ -502,7 +518,33 @@ class RemoveGuardianCodeSerializer(serializers.ModelSerializer): model = Junior fields = ('id', ) def update(self, instance, validated_data): - instance.guardian_code = None - instance.guardian_code_status = str(NUMBER['one']) + guardian_code = self.context['guardian_code'] + if guardian_code in instance.guardian_code: + if instance.guardian_code and ('-' in instance.guardian_code): + instance.guardian_code.remove('-') + if instance.guardian_code_status and ('-' in instance.guardian_code_status): + instance.guardian_code_status.remove('-') + index = instance.guardian_code.index(guardian_code) + instance.guardian_code.remove(guardian_code) + instance.guardian_code_status.pop(index) + else: + raise serializers.ValidationError({"error":ERROR_CODE['2082'],"code":"400", "status":"failed"}) instance.save() return instance + +class FAQSerializer(serializers.ModelSerializer): + """FAQ Serializer""" + + class Meta(object): + """meta info""" + model = FAQ + fields = ('id', 'question', 'description') + +class CreateArticleCardSerializer(serializers.ModelSerializer): + """Article card Serializer""" + + class Meta(object): + """meta info""" + model = ArticleCard + fields = ('id', 'article') + diff --git a/junior/urls.py b/junior/urls.py index b145d4f..3c597c3 100644 --- a/junior/urls.py +++ b/junior/urls.py @@ -6,7 +6,7 @@ from .views import (UpdateJuniorProfile, ValidateGuardianCode, JuniorListAPIView CompleteJuniorTaskAPIView, JuniorPointsListAPIView, ValidateReferralCode, InviteGuardianAPIView, StartTaskAPIView, ReAssignJuniorTaskAPIView, StartArticleAPIView, StartAssessmentAPIView, CheckAnswerAPIView, CompleteArticleAPIView, ReadArticleCardAPIView, - CreateArticleCardAPIView, RemoveGuardianCodeAPIView) + CreateArticleCardAPIView, RemoveGuardianCodeAPIView, FAQViewSet) """Third party import""" from rest_framework import routers @@ -51,6 +51,8 @@ router.register('start-assessment', StartAssessmentAPIView, basename='start-asse router.register('check-answer', CheckAnswerAPIView, basename='check-answer') # start article""" router.register('create-article-card', CreateArticleCardAPIView, basename='create-article-card') +# FAQ API +router.register('faq', FAQViewSet, basename='faq') # Define url pattern""" urlpatterns = [ path('api/v1/', include(router.urls)), @@ -60,5 +62,5 @@ urlpatterns = [ path('api/v1/reassign-task/', ReAssignJuniorTaskAPIView.as_view()), path('api/v1/complete-article/', CompleteArticleAPIView.as_view()), path('api/v1/read-article-card/', ReadArticleCardAPIView.as_view()), - path('api/v1/remove-guardian-code-request/', RemoveGuardianCodeAPIView.as_view()), + path('api/v1/remove-guardian-code-request/', RemoveGuardianCodeAPIView.as_view()) ] diff --git a/junior/utils.py b/junior/utils.py index ba177a8..eac6ac9 100644 --- a/junior/utils.py +++ b/junior/utils.py @@ -5,7 +5,8 @@ from django.conf import settings from templated_email import send_templated_mail from .models import JuniorPoints from base.constants import NUMBER -from django.db.models import F +from django.db.models import F, Window +from django.db.models.functions.window import Rank # junior notification # email for sending email # when guardian create junior profile @@ -14,6 +15,8 @@ from django.db.models import F # being part of the zod bank and access the platform # define junior notification email # junior approval email +from celery import shared_task +@shared_task() def junior_notification_email(recipient_email, full_name, email, password): """Notification email""" from_email = settings.EMAIL_FROM_ADDRESS @@ -32,7 +35,7 @@ def junior_notification_email(recipient_email, full_name, email, password): } ) return full_name - +@shared_task() def junior_approval_mail(guardian, full_name): """junior approval mail""" from_email = settings.EMAIL_FROM_ADDRESS @@ -59,3 +62,21 @@ def update_positions_based_on_points(): junior_point.position = position junior_point.save() position += NUMBER['one'] + + +def get_junior_leaderboard_rank(junior_obj): + """ + to get junior's position/rank + :param junior_obj: + :return: junior's position/rank + """ + queryset = JuniorPoints.objects.filter( + junior__is_verified=True + ).select_related('junior', 'junior__auth').annotate(rank=Window( + expression=Rank(), + order_by=[F('total_points').desc(), 'junior__created_at'] + )).order_by('-total_points', 'junior__created_at') + + junior = next((query for query in queryset if query.junior == junior_obj), None) + + return junior.rank if junior else None diff --git a/junior/views.py b/junior/views.py index a86083d..64aaffb 100644 --- a/junior/views.py +++ b/junior/views.py @@ -10,8 +10,12 @@ from django.db.models import F import datetime import requests -"""Django app import""" +from rest_framework.viewsets import GenericViewSet, mixins +"""Django app import""" +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from drf_yasg.views import get_schema_view # Import guardian's model, # Import junior's model, # Import account's model, @@ -30,20 +34,20 @@ import requests # Import constants from django.db.models import Sum from junior.models import (Junior, JuniorPoints, JuniorGuardianRelationship, JuniorArticlePoints, JuniorArticle, - JuniorArticleCard) + JuniorArticleCard, FAQ) from .serializers import (CreateJuniorSerializer, JuniorDetailListSerializer, AddJuniorSerializer, RemoveJuniorSerializer, CompleteTaskSerializer, JuniorPointsSerializer, AddGuardianSerializer, StartTaskSerializer, ReAssignTaskSerializer, - RemoveGuardianCodeSerializer) + RemoveGuardianCodeSerializer, FAQSerializer, CreateArticleCardSerializer) from guardian.models import Guardian, JuniorTask from guardian.serializers import TaskDetailsSerializer, TaskDetailsjuniorSerializer from base.messages import ERROR_CODE, SUCCESS_CODE -from base.constants import NUMBER, ARTICLE_STATUS +from base.constants import NUMBER, ARTICLE_STATUS, none, GUARDIAN from account.utils import custom_response, custom_error_response from guardian.utils import upload_image_to_alibaba from .utils import update_positions_based_on_points from notifications.utils import send_notification -from notifications.constants import REMOVE_JUNIOR +from notifications.constants import REMOVE_JUNIOR, ARTICLE_REWARD_POINTS, ASSOCIATE_EXISTING_JUNIOR from web_admin.models import Article, ArticleSurvey, SurveyOption, ArticleCard from web_admin.serializers.article_serializer import (ArticleSerializer, ArticleListSerializer, StartAssessmentSerializer) @@ -63,13 +67,14 @@ from web_admin.serializers.article_serializer import (ArticleSerializer, Article # Start task # by junior API # Create your views here. -class UpdateJuniorProfile(viewsets.ViewSet): +class UpdateJuniorProfile(viewsets.ModelViewSet): """Update junior profile""" serializer_class = CreateJuniorSerializer permission_classes = [IsAuthenticated] + http_method_names = ('post',) def create(self, request, *args, **kwargs): - """Use CreateJuniorSerializer""" + """Create Junior Profile""" try: request_data = request.data image = request.data.get('image') @@ -94,14 +99,22 @@ class UpdateJuniorProfile(viewsets.ViewSet): return custom_response(None, serializer.data, response_status=status.HTTP_200_OK) return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST) except Exception as e: - return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) + if e.detail: + error_detail = e.detail.get('error', None) + else: + error_detail = str(e) + return custom_error_response(error_detail, response_status=status.HTTP_400_BAD_REQUEST) -class ValidateGuardianCode(viewsets.ViewSet): +class ValidateGuardianCode(viewsets.ModelViewSet): """Check guardian code exist or not""" permission_classes = [IsAuthenticated] + http_method_names = ('get',) def list(self, request, *args, **kwargs): - """check guardian code""" + """check guardian code + Params + "guardian_code" + """ try: guardian_code = self.request.GET.get('guardian_code').split(',') for code in guardian_code: @@ -132,14 +145,15 @@ class JuniorListAPIView(viewsets.ModelViewSet): def list(self, request, *args, **kwargs): """ junior list""" try: - update_positions_based_on_points() + # update_positions_based_on_points, function removed guardian_data = Guardian.objects.filter(user__email=request.user).last() # fetch junior object if guardian_data: queryset = self.get_queryset() queryset = queryset.filter(guardian_code__icontains=str(guardian_data.guardian_code)) # use JuniorDetailListSerializer serializer - serializer = JuniorDetailListSerializer(queryset, many=True) + serializer = JuniorDetailListSerializer(queryset, context={"guardian_code": + guardian_data.guardian_code}, many=True) return custom_response(None, serializer.data, response_status=status.HTTP_200_OK) return custom_error_response(ERROR_CODE['2045'], response_status=status.HTTP_200_OK) except Exception as e: @@ -153,7 +167,14 @@ class AddJuniorAPIView(viewsets.ModelViewSet): http_method_names = ('post',) def create(self, request, *args, **kwargs): - """ junior list""" + """ add junior + { "gender":"1", + "first_name":"abc", + "last_name":"xyz", + "dob":"2023-12-12", + "relationship":"2", + "email":"abc@yopmail.com" + }""" try: info_data = {'user': request.user, 'relationship': str(request.data['relationship']), 'email': request.data['email'], 'first_name': request.data['first_name'], @@ -169,7 +190,13 @@ class AddJuniorAPIView(viewsets.ModelViewSet): image_url = upload_image_to_alibaba(profile_image, filename) info_data.update({"image": image_url}) if user := User.objects.filter(username=request.data['email']).first(): - self.associate_guardian(user) + data = self.associate_guardian(user) + if data == none: + return custom_error_response(ERROR_CODE['2077'], response_status=status.HTTP_400_BAD_REQUEST) + elif not data: + return custom_error_response(ERROR_CODE['2076'], response_status=status.HTTP_400_BAD_REQUEST) + elif data == "Max": + return custom_error_response(ERROR_CODE['2081'], response_status=status.HTTP_400_BAD_REQUEST) return custom_response(SUCCESS_CODE['3021'], response_status=status.HTTP_200_OK) # use AddJuniorSerializer serializer serializer = AddJuniorSerializer(data=request.data, context=info_data) @@ -182,18 +209,37 @@ class AddJuniorAPIView(viewsets.ModelViewSet): return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) def associate_guardian(self, user): - junior = Junior.objects.filter(auth=user).first() + junior = Junior.objects.filter(auth__email=self.request.data['email']).first() guardian = Guardian.objects.filter(user=self.request.user).first() - junior.guardian_code = [guardian.guardian_code] - junior.guardian_code_status = str(NUMBER['two']) + if junior.guardian_code and ('-' in junior.guardian_code): + junior.guardian_code.remove('-') + if not junior: + return none + if junior.guardian_code and (guardian.guardian_code in junior.guardian_code): + return False + if not junior.guardian_code: + junior.guardian_code = [guardian.guardian_code] + if type(junior.guardian_code) is list and len(junior.guardian_code) < 3: + junior.guardian_code.append(guardian.guardian_code) + else: + return "Max" + if junior.guardian_code_status and ('-' in junior.guardian_code_status): + junior.guardian_code_status.remove('-') + if not junior.guardian_code_status: + junior.guardian_code_status = [str(NUMBER['two'])] + else: + junior.guardian_code_status.append(str(NUMBER['two'])) junior.save() - JuniorGuardianRelationship.objects.get_or_create(guardian=guardian, junior=junior, - relationship=str(self.request.data['relationship'])) + jun_data, created = JuniorGuardianRelationship.objects.get_or_create(guardian=guardian, junior=junior) + if jun_data: + jun_data.relationship = str(self.request.data['relationship']) + jun_data.save() + send_notification.delay(ASSOCIATE_EXISTING_JUNIOR, self.request.user.id, GUARDIAN, junior.auth.id, {}) return True class InvitedJuniorAPIView(viewsets.ModelViewSet): - """Junior list of assosicated guardian""" + """Invited Junior list of assosicated guardian""" serializer_class = JuniorDetailListSerializer permission_classes = [IsAuthenticated] @@ -207,7 +253,8 @@ class InvitedJuniorAPIView(viewsets.ModelViewSet): is_invited=True) return junior_queryset def list(self, request, *args, **kwargs): - """ junior list""" + """ Invited Junior list of assosicated guardian + No Params""" try: queryset = self.get_queryset() paginator = self.pagination_class() @@ -221,12 +268,25 @@ class InvitedJuniorAPIView(viewsets.ModelViewSet): class FilterJuniorAPIView(viewsets.ModelViewSet): - """Update guardian profile""" + """filter junior profile""" serializer_class = JuniorDetailListSerializer permission_classes = [IsAuthenticated] pagination_class = PageNumberPagination http_method_names = ('get',) + @swagger_auto_schema( + manual_parameters=[ + # Example of a query parameter + openapi.Parameter( + 'title', + openapi.IN_QUERY, + description='title of the name', + type=openapi.TYPE_STRING, + ), + # Add more parameters as needed + ] + ) + def get_queryset(self): """Get the queryset for the view""" title = self.request.GET.get('title') @@ -237,7 +297,7 @@ class FilterJuniorAPIView(viewsets.ModelViewSet): return queryset def list(self, request, *args, **kwargs): - """Create guardian profile""" + """Filter junior""" try: queryset = self.get_queryset() paginator = self.pagination_class() @@ -251,7 +311,9 @@ class FilterJuniorAPIView(viewsets.ModelViewSet): class RemoveJuniorAPIView(views.APIView): - """Remove junior API""" + """Remove junior API + Params + id=37""" serializer_class = RemoveJuniorSerializer model = Junior permission_classes = [IsAuthenticated] @@ -265,11 +327,13 @@ class RemoveJuniorAPIView(views.APIView): guardian_code__icontains=str(guardian.guardian_code)).last() if junior_queryset: # use RemoveJuniorSerializer serializer - serializer = RemoveJuniorSerializer(junior_queryset, data=request.data, partial=True) + serializer = RemoveJuniorSerializer(junior_queryset, context={"guardian_code":guardian.guardian_code}, + data=request.data, partial=True) if serializer.is_valid(): # save serializer serializer.save() - send_notification.delay(REMOVE_JUNIOR, None, junior_queryset.auth.id, {}) + JuniorGuardianRelationship.objects.filter(guardian=guardian, junior=junior_queryset).delete() + send_notification.delay(REMOVE_JUNIOR, None, None, junior_queryset.auth.id, {}) return custom_response(SUCCESS_CODE['3022'], serializer.data, response_status=status.HTTP_200_OK) return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST) else: @@ -279,44 +343,46 @@ class RemoveJuniorAPIView(views.APIView): class JuniorTaskListAPIView(viewsets.ModelViewSet): - """Update guardian profile""" + """Junior task list""" serializer_class = TaskDetailsjuniorSerializer permission_classes = [IsAuthenticated] + filter_backends = (SearchFilter,) + search_fields = ['task_name', ] pagination_class = PageNumberPagination http_method_names = ('get',) + def get_queryset(self): + queryset = JuniorTask.objects.filter(junior__auth=self.request.user + ).prefetch_related('junior', 'junior__auth' + ).order_by('due_date', 'created_at') + + queryset = self.filter_queryset(queryset) + return queryset + def list(self, request, *args, **kwargs): - """Create guardian profile""" + """Junior task list + status=0 + search='title' + page=1""" try: status_value = self.request.GET.get('status') - search = self.request.GET.get('search') - if search and str(status_value) == '0': - # search with title and for all task list - queryset = JuniorTask.objects.filter(junior__auth=request.user, - task_name__icontains=search).order_by('due_date', 'created_at') - elif search and str(status_value) != '0': - # search with title and fetch task list with status wise - queryset = JuniorTask.objects.filter(junior__auth=request.user, task_name__icontains=search, - task_status=status_value).order_by('due_date', 'created_at') - if search is None and str(status_value) == '0': - # fetch all task list - queryset = JuniorTask.objects.filter(junior__auth=request.user).order_by('due_date', 'created_at') - elif search is None and str(status_value) != '0': - # fetch task list with status wise - queryset = JuniorTask.objects.filter(junior__auth=request.user, - task_status=status_value).order_by('due_date','created_at') + queryset = self.get_queryset() + if status_value and status_value != '0': + queryset = queryset.filter(task_status=status_value) paginator = self.pagination_class() # use Pagination paginated_queryset = paginator.paginate_queryset(queryset, request) - # use TaskDetailsSerializer serializer - serializer = TaskDetailsjuniorSerializer(paginated_queryset, many=True) + # use TaskDetails juniorSerializer serializer + serializer = self.serializer_class(paginated_queryset, many=True) return custom_response(None, serializer.data, response_status=status.HTTP_200_OK) except Exception as e: return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) class CompleteJuniorTaskAPIView(views.APIView): - """Update junior task API""" + """Payload + task_id + image""" serializer_class = CompleteTaskSerializer model = JuniorTask permission_classes = [IsAuthenticated] @@ -334,8 +400,11 @@ class CompleteJuniorTaskAPIView(views.APIView): image_url = upload_image_to_alibaba(image, filename) # fetch junior query - task_queryset = JuniorTask.objects.filter(id=task_id, junior__auth__email=self.request.user).last() + task_queryset = JuniorTask.objects.filter(id=task_id, junior__auth__email=self.request.user + ).select_related('guardian', 'junior').last() if task_queryset: + if task_queryset.junior.is_deleted or not task_queryset.junior.is_active: + return custom_error_response(ERROR_CODE['2074'], response_status=status.HTTP_400_BAD_REQUEST) # use CompleteTaskSerializer serializer if task_queryset.task_status in [str(NUMBER['four']), str(NUMBER['five'])]: """Already request send """ @@ -358,9 +427,10 @@ class JuniorPointsListAPIView(viewsets.ModelViewSet): http_method_names = ('get',) def list(self, request, *args, **kwargs): - """profile view""" + """Junior Points + No Params""" try: - update_positions_based_on_points() + # update_positions_based_on_points, function removed queryset = JuniorPoints.objects.filter(junior__auth__email=self.request.user).last() # update position of junior serializer = JuniorPointsSerializer(queryset) @@ -369,7 +439,7 @@ class JuniorPointsListAPIView(viewsets.ModelViewSet): return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) -class ValidateReferralCode(viewsets.ViewSet): +class ValidateReferralCode(viewsets.ModelViewSet): """Check guardian code exist or not""" permission_classes = [IsAuthenticated] http_method_names = ('get',) @@ -404,7 +474,13 @@ class InviteGuardianAPIView(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] http_method_names = ('post',) def create(self, request, *args, **kwargs): - """ junior list""" + """ add guardian + { + "first_name":"abc", + "last_name":"xyz", + "email":"abc@yopmail.com", + "relationship":2 + }""" try: if request.data['email'] == '': return custom_error_response(ERROR_CODE['2062'], response_status=status.HTTP_400_BAD_REQUEST) @@ -422,7 +498,11 @@ class InviteGuardianAPIView(viewsets.ModelViewSet): class StartTaskAPIView(views.APIView): - """Update junior task API""" + """Update junior task API + Paylod + { + "task_id":28 + }""" serializer_class = StartTaskSerializer model = JuniorTask permission_classes = [IsAuthenticated] @@ -446,7 +526,13 @@ class StartTaskAPIView(views.APIView): return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) class ReAssignJuniorTaskAPIView(views.APIView): - """Update junior task API""" + """Update junior task API + Payload + { + "task_id":34, + "due_date":"2023-08-22" + } + """ serializer_class = ReAssignTaskSerializer model = JuniorTask permission_classes = [IsAuthenticated] @@ -475,7 +561,10 @@ class StartArticleAPIView(viewsets.ModelViewSet): http_method_names = ('post',) def create(self, request, *args, **kwargs): - """ junior list""" + """ Payload + { + "article_id":"2" + }""" try: junior_instance = Junior.objects.filter(auth=self.request.user).last() article_id = request.data.get('article_id') @@ -497,7 +586,7 @@ class StartArticleAPIView(viewsets.ModelViewSet): return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) class StartAssessmentAPIView(viewsets.ModelViewSet): - """Junior Points viewset""" + """Question answer viewset""" serializer_class = StartAssessmentSerializer permission_classes = [IsAuthenticated] http_method_names = ('get',) @@ -510,7 +599,9 @@ class StartAssessmentAPIView(viewsets.ModelViewSet): ).order_by('-created_at') return article def list(self, request, *args, **kwargs): - """profile view""" + """Params + article_id + """ try: queryset = self.get_queryset() @@ -522,7 +613,9 @@ class StartAssessmentAPIView(viewsets.ModelViewSet): return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) class CheckAnswerAPIView(viewsets.ModelViewSet): - """Junior Points viewset""" + """Params + question_id=1 + answer_id=1""" permission_classes = [IsAuthenticated] http_method_names = ('get',) @@ -531,13 +624,16 @@ class CheckAnswerAPIView(viewsets.ModelViewSet): article = ArticleSurvey.objects.filter(id=question_id).last() return article def list(self, request, *args, **kwargs): - """profile view""" + """ Params + question_id=1 + answer_id=1 + """ try: answer_id = self.request.GET.get('answer_id') current_page = self.request.GET.get('current_page') queryset = self.get_queryset() - submit_ans = SurveyOption.objects.filter(id=answer_id, is_answer=True).last() + submit_ans = SurveyOption.objects.filter(id=answer_id).last() junior_article_points = JuniorArticlePoints.objects.filter(junior__auth=self.request.user, question=queryset) if submit_ans: @@ -555,7 +651,9 @@ class CheckAnswerAPIView(viewsets.ModelViewSet): return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) class CompleteArticleAPIView(views.APIView): - """Remove junior API""" + """Params + article_id + """ permission_classes = [IsAuthenticated] http_method_names = ('put', 'get',) def put(self, request, format=None): @@ -569,7 +667,8 @@ class CompleteArticleAPIView(views.APIView): return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) def get(self, request, *args, **kwargs): - """ junior list""" + """ Params + article_id=1""" try: article_id = self.request.GET.get('article_id') total_earn_points = JuniorArticlePoints.objects.filter(junior__auth=request.user, @@ -577,18 +676,24 @@ class CompleteArticleAPIView(views.APIView): is_answer_correct=True).aggregate( total_earn_points=Sum('earn_points'))['total_earn_points'] data = {"total_earn_points":total_earn_points} + send_notification.delay(ARTICLE_REWARD_POINTS, None, None, + request.user.id, {'points': total_earn_points}) return custom_response(SUCCESS_CODE['3042'], data, response_status=status.HTTP_200_OK) except Exception as e: return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) class ReadArticleCardAPIView(views.APIView): - """Remove junior API""" + """Read article card API""" permission_classes = [IsAuthenticated] http_method_names = ('put',) def put(self, request, *args, **kwargs): - """ junior list""" + """ Read article card + Payload + {"article_id":"1", + "article_card":"2", + "current_page":"2"}""" try: junior_instance = Junior.objects.filter(auth=self.request.user).last() article = self.request.data.get('article_id') @@ -606,11 +711,14 @@ class ReadArticleCardAPIView(views.APIView): class CreateArticleCardAPIView(viewsets.ModelViewSet): """Start article""" + serializer_class = CreateArticleCardSerializer permission_classes = [IsAuthenticated] http_method_names = ('post',) def create(self, request, *args, **kwargs): - """ junior list""" + """ create article card + Params + {"article_id":1}""" try: junior_instance = Junior.objects.filter(auth=self.request.user).last() article_id = request.data.get('article_id') @@ -629,23 +737,75 @@ class CreateArticleCardAPIView(viewsets.ModelViewSet): return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) class RemoveGuardianCodeAPIView(views.APIView): - """Update junior task API""" + """Remove guardian code request API + Payload + {"guardian_code" + :"GRD037" + }""" serializer_class = RemoveGuardianCodeSerializer permission_classes = [IsAuthenticated] def put(self, request, format=None): try: + guardian_code = self.request.data.get("guardian_code") + guardian_data = Guardian.objects.filter(guardian_code=guardian_code).last() junior_queryset = Junior.objects.filter(auth=self.request.user).last() if junior_queryset: # use RemoveGuardianCodeSerializer serializer - serializer = RemoveGuardianCodeSerializer(junior_queryset, data=request.data, partial=True) + serializer = RemoveGuardianCodeSerializer(junior_queryset, context = {"guardian_code":guardian_code}, + data=request.data, partial=True) if serializer.is_valid(): # save serializer serializer.save() + JuniorGuardianRelationship.objects.filter(guardian=guardian_data, junior=junior_queryset).delete() return custom_response(SUCCESS_CODE['3044'], response_status=status.HTTP_200_OK) return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST) else: # task in another state return custom_error_response(ERROR_CODE['2047'], response_status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + error_detail = e.detail.get('error', None) + return custom_error_response(error_detail, response_status=status.HTTP_400_BAD_REQUEST) + + +class FAQViewSet(GenericViewSet, mixins.CreateModelMixin, + mixins.ListModelMixin): + """FAQ view set""" + + serializer_class = FAQSerializer + permission_classes = [IsAuthenticated] + http_method_names = ['get', 'post'] + + def get_queryset(self): + return FAQ.objects.all() + + def create(self, request, *args, **kwargs): + """ + faq create api method + :param request: + :param args: question, description + :param kwargs: + :return: success message + """ + obj_data = [FAQ(**item) for item in request.data] + try: + FAQ.objects.bulk_create(obj_data) + return custom_response(SUCCESS_CODE["3045"], response_status=status.HTTP_200_OK) except Exception as e: return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) + + + def list(self, request, *args, **kwargs): + """ + article list api method + :param request: + :param args: + :param kwargs: + :return: list of article + """ + queryset = self.get_queryset() + paginator = self.pagination_class() + paginated_queryset = paginator.paginate_queryset(queryset, request) + serializer = self.serializer_class(paginated_queryset, many=True) + return custom_response(None, data=serializer.data, response_status=status.HTTP_200_OK) + diff --git a/notifications/admin.py b/notifications/admin.py index 382e97b..c7cc895 100644 --- a/notifications/admin.py +++ b/notifications/admin.py @@ -10,3 +10,4 @@ from notifications.models import Notification class NotificationAdmin(admin.ModelAdmin): """Notification Admin""" list_display = ['id', 'notification_type', 'notification_to', 'data', 'is_read'] + list_filter = ['notification_type'] diff --git a/notifications/constants.py b/notifications/constants.py index b861142..85b0d2d 100644 --- a/notifications/constants.py +++ b/notifications/constants.py @@ -1,19 +1,26 @@ """ notification constants file """ -from base.constants import NUMBER -REGISTRATION = NUMBER['one'] -TASK_CREATED = NUMBER['two'] -INVITED_GUARDIAN = NUMBER['three'] -APPROVED_JUNIOR = NUMBER['four'] -REFERRAL_POINTS = NUMBER['five'] -TASK_POINTS = NUMBER['six'] -TASK_REJECTED = NUMBER['seven'] -SKIPPED_PROFILE_SETUP = NUMBER['eight'] -TASK_SUBMITTED = NUMBER['nine'] -TASK_ACTION = NUMBER['ten'] -LEADERBOARD_RANKING = NUMBER['eleven'] -REMOVE_JUNIOR = NUMBER['twelve'] +REGISTRATION = 1 +ASSOCIATE_REQUEST = 3 +ASSOCIATE_REJECTED = 4 +ASSOCIATE_APPROVED = 5 +REFERRAL_POINTS = 6 +ASSOCIATE_JUNIOR = 7 +ASSOCIATE_EXISTING_JUNIOR = 8 + +TASK_ASSIGNED = 9 +TASK_ACTION = 10 +TASK_REJECTED = 11 +TASK_APPROVED = 12 +PENDING_TASK_EXPIRING = 13 +IN_PROGRESS_TASK_EXPIRING = 14 +TOP_JUNIOR = 15 + +NEW_ARTICLE_PUBLISHED = 16 +ARTICLE_REWARD_POINTS = 17 +REMOVE_JUNIOR = 18 + TEST_NOTIFICATION = 99 NOTIFICATION_DICT = { @@ -21,52 +28,102 @@ NOTIFICATION_DICT = { "title": "Successfully registered!", "body": "You have registered successfully. Now login and complete your profile." }, - TASK_CREATED: { - "title": "Task created!", - "body": "Task created successfully." + # user will receive notification as soon junior + # sign up application using their guardian code for association + ASSOCIATE_REQUEST: { + "title": "Associate request!", + "body": "You have request from {from_user} to associate with you." }, - INVITED_GUARDIAN: { - "title": "Invite guardian", - "body": "Invite guardian successfully" + # Juniors will receive notification when + # custodians reject their request for associate + ASSOCIATE_REJECTED: { + "title": "Associate request rejected!", + "body": "Your request to associate has been rejected by {from_user}." }, - APPROVED_JUNIOR: { - "title": "Approve junior", - "body": "You have request from junior to associate with you" + # Juniors will receive notification when + # custodians approve their request for associate + ASSOCIATE_APPROVED: { + "title": "Associate request approved!", + "body": "Your request to associate has been approved by {from_user}." }, + # Juniors will receive Notifications + # for every Points earned by referrals REFERRAL_POINTS: { - "title": "Earn Referral points", + "title": "Earn Referral points!", "body": "You earn 5 points for referral." }, - TASK_POINTS: { - "title": "Earn Task points!", - "body": "You earn 5 points for task." + # Juniors will receive notification + # once any custodians add them in their account + ASSOCIATE_JUNIOR: { + "title": "Profile already setup!", + "body": "Your guardian has already setup your profile." }, - TASK_REJECTED: { - "title": "Task rejected!", - "body": "Your task has been rejected." + ASSOCIATE_EXISTING_JUNIOR: { + "title": "Associated to guardian", + "body": "Your are associated to your guardian {from_user}." }, - SKIPPED_PROFILE_SETUP: { - "title": "Skipped profile setup!", - "body": "Your guardian has been setup your profile." - }, - TASK_SUBMITTED: { - "title": "Task submitted!", - "body": "Your task has been submitted successfully." + # Juniors will receive Notification + # for every Task Assign by Custodians + TASK_ASSIGNED: { + "title": "New task assigned!", + "body": "{from_user} has assigned you a new task." }, + # Guardian will receive notification as soon + # as junior send task for approval TASK_ACTION: { - "title": "Task approval!", - "body": "You have request for task approval." + "title": "Task completion approval!", + "body": "{from_user} completed their task {task_name}." }, - LEADERBOARD_RANKING: { - "title": "Leader board rank!", - "body": "Your rank is ." + # Juniors will receive notification as soon + # as their task is rejected by custodians + TASK_REJECTED: { + "title": "Task completion rejected!", + "body": "Your task completion request has been rejected by {from_user}." }, + # Juniors will receive notification as soon as their task is approved by custodians + # and for every Points earned by Task completion + TASK_APPROVED: { + "title": "Task completion approved!", + "body": "Your task completion request has been approved by {from_user}. " + "Also you earned 5 points for successful completion." + }, + # Juniors will receive notification when their task end date about to end + PENDING_TASK_EXPIRING: { + "title": "Task expiring soon!", + "body": "Your task {task_name} is expiring soon. Please complete it." + }, + # User will receive notification when their assigned task is about to end + # and juniors have not performed any action + IN_PROGRESS_TASK_EXPIRING: { + "title": "Task expiring soon!", + "body": "{from_user} didn't take any action on assigned task {task_name} and it's expiring soon. " + "Please assist to complete it." + }, + # Juniors will receive Notification + # related to Leaderboard progress + TOP_JUNIOR: { + "title": "Leaderboard topper!", + "body": "{from_user} is on top in leaderboard with {points} points." + }, + # Juniors will receive notification + # when admin add any new financial learnings + NEW_ARTICLE_PUBLISHED: { + "title": "Time to read!", + "body": "A new article has been published." + }, + # Juniors will receive notification when they earn points by reading financial Learning + ARTICLE_REWARD_POINTS: { + "title": "Article reward points!", + "body": "You are rewarded with {points} points for reading article and answering questions. " + }, + # Juniors will receive notification as soon as their custodians remove them from account REMOVE_JUNIOR: { "title": "Disassociate by guardian!", - "body": "Your guardian disassociate you ." + "body": "Your guardian has disassociated you." }, + # Test notification TEST_NOTIFICATION: { "title": "Test Notification", - "body": "This notification is for testing purpose" + "body": "This notification is for testing purpose from {from_user}." } } diff --git a/notifications/serializers.py b/notifications/serializers.py index a061369..2f0222f 100644 --- a/notifications/serializers.py +++ b/notifications/serializers.py @@ -35,10 +35,13 @@ class NotificationListSerializer(serializers.ModelSerializer): class Meta(object): """meta info""" model = Notification - fields = ['id', 'data', 'is_read'] + fields = ['id', 'data', 'is_read', 'created_at'] + class ReadNotificationSerializer(serializers.ModelSerializer): """User task Serializer""" + id = serializers.ListSerializer(child=serializers.IntegerField()) + class Meta(object): """Meta class""" model = Notification diff --git a/notifications/urls.py b/notifications/urls.py index 713aae3..b184d02 100644 --- a/notifications/urls.py +++ b/notifications/urls.py @@ -6,7 +6,7 @@ from django.urls import path, include from rest_framework import routers # local imports -from notifications.views import NotificationViewSet, ReadNotification +from notifications.views import NotificationViewSet # initiate router router = routers.SimpleRouter() @@ -15,5 +15,4 @@ router.register('notifications', NotificationViewSet, basename='notifications') urlpatterns = [ path('api/v1/', include(router.urls)), - path('api/v1/read-notification/', ReadNotification.as_view()), ] diff --git a/notifications/utils.py b/notifications/utils.py index ba980e6..bd2a8bd 100644 --- a/notifications/utils.py +++ b/notifications/utils.py @@ -8,21 +8,24 @@ from firebase_admin.messaging import Message, Notification as FirebaseNotificati # django imports from django.contrib.auth import get_user_model - -from account.models import UserNotification -from notifications.constants import NOTIFICATION_DICT -from notifications.models import Notification +from django.db.models import Q # local imports - +from account.models import UserNotification +from account.utils import get_user_full_name +from base.constants import GUARDIAN, JUNIOR +from guardian.models import Guardian, JuniorTask +from junior.models import Junior +from notifications.constants import NOTIFICATION_DICT +from notifications.models import Notification User = get_user_model() def register_fcm_token(user_id, registration_id, device_id, device_type): """ used to register the fcm device token""" - device, _ = FCMDevice.objects.update_or_create(device_id=device_id, - defaults={'user_id': user_id, 'type': device_type, + device, _ = FCMDevice.objects.update_or_create(user_id=user_id, + defaults={'device_id': device_id, 'type': device_type, 'active': True, 'registration_id': registration_id}) return device @@ -39,30 +42,140 @@ def remove_fcm_token(user_id: int, access_token: str, registration_id) -> None: print(e) -def get_basic_detail(notification_type, from_user_id, to_user_id): - """ used to get the basic details """ - notification_data = NOTIFICATION_DICT[notification_type] - from_user = User.objects.get(id=from_user_id) if from_user_id else None - to_user = User.objects.get(id=to_user_id) - return notification_data, from_user, to_user +def get_from_user_details(from_user_id, from_user_type): + """ + used to get from user details + """ + from_user = None + from_user_name = None + from_user_image = None + if from_user_id: + if from_user_type == GUARDIAN: + guardian = Guardian.objects.filter(user_id=from_user_id).select_related('user').first() + from_user = guardian.user + from_user_name = get_user_full_name(from_user) + from_user_image = guardian.image + elif from_user_type == JUNIOR: + junior = Junior.objects.filter(auth_id=from_user_id).select_related('auth').first() + from_user = junior.auth + from_user_name = get_user_full_name(from_user) + from_user_image = junior.image + return from_user_name, from_user_image, from_user + + +def get_notification_data(notification_type, from_user_id, from_user_type, to_user_id, extra_data): + """ + get notification and push data + :param from_user_type: GUARDIAN or JUNIOR + :param notification_type: notification_type + :param from_user_id: from_user obj + :param to_user_id: to_user obj + :param extra_data: any extra data provided + :return: notification and push data + """ + push_data = NOTIFICATION_DICT[notification_type].copy() + notification_data = push_data.copy() + task_name = None + points = extra_data.get('points', None) + if 'task_id' in extra_data: + task = JuniorTask.objects.filter(id=extra_data.get('task_id')).first() + task_name = task.task_name + extra_data['task_name'] = task_name + extra_data['task_image'] = task.image if task.image else task.default_image + + from_user_name, from_user_image, from_user = get_from_user_details(from_user_id, from_user_type) + + push_data['body'] = push_data['body'].format(from_user=from_user_name, task_name=task_name, points=points) + notification_data['body'] = notification_data['body'].format(from_user=from_user_name, + task_name=task_name, points=points) + notification_data['from_user'] = from_user_name + notification_data['from_user_image'] = from_user_image + + notification_data.update(extra_data) + to_user = User.objects.filter(id=to_user_id).first() + return notification_data, push_data, from_user, to_user @shared_task() -def send_notification(notification_type, from_user_id, to_user_id, extra_data): - """ used to send the push for the given notification type """ - (notification_data, from_user, to_user) = get_basic_detail(notification_type, from_user_id, to_user_id) +def send_notification(notification_type, from_user_id, from_user_type, to_user_id, extra_data): + """ + used to send the push for the given notification type + """ + notification_data, push_data, from_user, to_user = get_notification_data(notification_type, from_user_id, + from_user_type, to_user_id, extra_data) user_notification_type = UserNotification.objects.filter(user=to_user).first() - data = notification_data + notification_data.update({'badge': Notification.objects.filter(notification_to=to_user, is_read=False).count()}) Notification.objects.create(notification_type=notification_type, notification_from=from_user, - notification_to=to_user, data=data) - if user_notification_type.push_notification: - data.update({'badge': Notification.objects.filter(notification_to=to_user, is_read=False).count()}) - send_push(to_user, data) + notification_to=to_user, data=notification_data) + if user_notification_type and user_notification_type.push_notification: + send_push(to_user, push_data) def send_push(user, data): """ used to send push notification to specific user """ - notification_data = data.pop('data', None) user.fcmdevice_set.filter(active=True).send_message( - Message(notification=FirebaseNotification(data['title'], data['body']), data=notification_data) + Message(notification=FirebaseNotification(data['title'], data['body']), data=data) ) + + +def send_multiple_push(queryset, data): + """ used to send same notification to multiple users """ + FCMDevice.objects.filter(user__in=queryset, active=True).send_message( + Message(notification=FirebaseNotification(data['title'], data['body']), data=data) + ) + + +@shared_task() +def send_notification_multiple_user(notification_type, from_user_id, from_user_type, + extra_data: dict = {}): + """ + used to send notification to multiple user for the given notification type + """ + to_user_list = User.objects.filter(junior_profile__is_verified=True, is_superuser=False + ).exclude(junior_profile__isnull=True, guardian_profile__isnull=True) + + notification_data, push_data, from_user, _ = get_notification_data(notification_type, from_user_id, + from_user_type, None, extra_data) + + notification_list = [] + for user in to_user_list: + notification_copy_data = notification_data.copy() + notification_copy_data.update( + {'badge': Notification.objects.filter(notification_to=user, is_read=False).count()}) + notification_list.append(Notification(notification_type=notification_type, + notification_to=user, + notification_from=from_user, + data=notification_copy_data)) + Notification.objects.bulk_create(notification_list) + to_user_list = to_user_list.filter(user_notification__push_notification=True) + send_multiple_push(to_user_list, push_data) + + +@shared_task() +def send_notification_to_guardian(notification_type, from_user_id, to_user_id, extra_data): + """ + :param notification_type: + :param from_user_id: + :param to_user_id: + :param extra_data: + :return: + """ + if from_user_id: + from_user = Junior.objects.filter(auth_id=from_user_id).first() + extra_data['from_user_image'] = from_user.image + send_notification(notification_type, from_user_id, to_user_id, extra_data) + + +@shared_task() +def send_notification_to_junior(notification_type, from_user_id, to_user_id, extra_data): + """ + :param notification_type: + :param from_user_id: + :param to_user_id: + :param extra_data: + :return: + """ + if from_user_id: + from_user = Guardian.objects.filter(user_id=from_user_id).first() + extra_data['from_user_image'] = from_user.image + send_notification(notification_type, from_user_id, to_user_id, extra_data) diff --git a/notifications/views.py b/notifications/views.py index c66d655..812502e 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -4,40 +4,39 @@ notifications views file # django imports from django.db.models import Q from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from rest_framework import viewsets, status, views + # local imports from account.utils import custom_response, custom_error_response from base.messages import SUCCESS_CODE, ERROR_CODE +from base.tasks import notify_task_expiry, notify_top_junior from notifications.constants import TEST_NOTIFICATION -# Import serializer from notifications.serializers import RegisterDevice, NotificationListSerializer, ReadNotificationSerializer from notifications.utils import send_notification -# Import model from notifications.models import Notification class NotificationViewSet(viewsets.GenericViewSet): - """ used to do the notification actions """ + """ + used to do the notification actions + """ serializer_class = NotificationListSerializer permission_classes = [IsAuthenticated, ] def list(self, request, *args, **kwargs) -> Response: - """ list the notifications """ + """ + to list user's notifications + :param request: + :return: + """ queryset = Notification.objects.filter(notification_to_id=request.auth.payload['user_id'] ).select_related('notification_to').order_by('-id') paginator = self.pagination_class() paginated_queryset = paginator.paginate_queryset(queryset, request) serializer = self.serializer_class(paginated_queryset, many=True) - self.mark_notifications_as_read(serializer.data) - return custom_response(None, serializer.data) - - @staticmethod - def mark_notifications_as_read(data): - """ used to mark notification queryset as read """ - ids = [obj['id'] for obj in data] - Notification.objects.filter(id__in=ids).update(is_read=True) + return custom_response(None, serializer.data, count=queryset.count()) @action(methods=['post'], detail=False, url_path='device', url_name='device', serializer_class=RegisterDevice) def fcm_registration(self, request): @@ -53,38 +52,20 @@ class NotificationViewSet(viewsets.GenericViewSet): @action(methods=['get'], detail=False, url_path='test', url_name='test') def send_test_notification(self, request): """ - to send test notification + to test send notification, task expiry, top junior :return: """ - send_notification.delay(TEST_NOTIFICATION, None, request.auth.payload['user_id'], {}) + notify_task_expiry() + notify_top_junior() + send_notification(TEST_NOTIFICATION, None, None, request.auth.payload['user_id'], + {}) return custom_response(SUCCESS_CODE["3000"]) - @action(methods=['get'], detail=False, url_path='list', url_name='list', - serializer_class=NotificationListSerializer) - def notification_list(self, request): + @action(methods=['patch'], url_path='mark-as-read', url_name='mark-as-read', detail=False, + serializer_class=ReadNotificationSerializer) + def mark_as_read(self, request, *args, **kwargs): """ notification list """ - try: - queryset = Notification.objects.filter(notification_to=request.user) - serializer = NotificationListSerializer(queryset, many=True) - return custom_response(None, serializer.data, response_status=status.HTTP_200_OK) - except Exception as e: - return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) - - -class ReadNotification(views.APIView): - """Update notification API""" - serializer_class = ReadNotificationSerializer - model = Notification - permission_classes = [IsAuthenticated] - - def put(self, request, format=None): - try: - notification_id = self.request.data.get('notification_id') - notification_queryset = Notification.objects.filter(id__in=notification_id, - notification_to=self.request.user).update(is_read=True) - if notification_queryset: - return custom_response(SUCCESS_CODE['3039'], response_status=status.HTTP_200_OK) - except Exception as e: - return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST) + Notification.objects.filter(id__in=request.data.get('id')).update(is_read=True) + return custom_response(SUCCESS_CODE['3039'], response_status=status.HTTP_200_OK) diff --git a/requirements.txt b/requirements.txt index f1540e7..624a176 100644 --- a/requirements.txt +++ b/requirements.txt @@ -99,3 +99,6 @@ uritemplate==4.1.1 urllib3==1.26.16 vine==5.0.0 wcwidth==0.2.6 + +pandas==2.0.3 +XlsxWriter==3.1.2 \ No newline at end of file diff --git a/web_admin/pagination.py b/web_admin/pagination.py index 6a6aff4..c2eed5e 100644 --- a/web_admin/pagination.py +++ b/web_admin/pagination.py @@ -1,13 +1,18 @@ """ web_admin pagination file """ +# third party imports from rest_framework.pagination import PageNumberPagination +from base.constants import NUMBER + class CustomPageNumberPagination(PageNumberPagination): """ custom paginator class """ - page_size = 10 # Set the desired page size + # Set the desired page size + page_size = NUMBER['ten'] page_size_query_param = 'page_size' - max_page_size = 100 # Set a maximum page size if needed + # Set a maximum page size if needed + max_page_size = NUMBER['hundred'] diff --git a/web_admin/serializers/analytics_serializer.py b/web_admin/serializers/analytics_serializer.py index fd85118..7871615 100644 --- a/web_admin/serializers/analytics_serializer.py +++ b/web_admin/serializers/analytics_serializer.py @@ -1,12 +1,25 @@ """ web_admin analytics serializer file """ +# third party imports from rest_framework import serializers +# django imports +from django.contrib.auth import get_user_model + +from account.utils import get_user_full_name +# local imports +from base.constants import USER_TYPE, JUNIOR + from junior.models import JuniorPoints, Junior +USER = get_user_model() + class JuniorLeaderboardSerializer(serializers.ModelSerializer): + """ + junior leaderboard serializer + """ name = serializers.SerializerMethodField() first_name = serializers.SerializerMethodField() last_name = serializers.SerializerMethodField() @@ -16,7 +29,7 @@ class JuniorLeaderboardSerializer(serializers.ModelSerializer): meta class """ model = Junior - fields = ('id', 'name', 'first_name', 'last_name', 'is_active', 'image') + fields = ('id', 'name', 'first_name', 'last_name', 'is_active', 'image', 'is_deleted') @staticmethod def get_name(obj): @@ -24,7 +37,7 @@ class JuniorLeaderboardSerializer(serializers.ModelSerializer): :param obj: junior object :return: full name """ - return f"{obj.auth.first_name} {obj.auth.last_name}" if obj.auth.last_name else obj.auth.first_name + return get_user_full_name(obj.auth) @staticmethod def get_first_name(obj): @@ -44,9 +57,94 @@ class JuniorLeaderboardSerializer(serializers.ModelSerializer): class LeaderboardSerializer(serializers.ModelSerializer): + """ + leaderboard serializer + """ + user_id = serializers.SerializerMethodField() + user_type = serializers.SerializerMethodField() junior = JuniorLeaderboardSerializer() rank = serializers.IntegerField() class Meta: + """ + meta class + """ model = JuniorPoints - fields = ('total_points', 'rank', 'junior') + fields = ('user_id', 'user_type', 'total_points', 'rank', 'junior') + + @staticmethod + def get_user_id(obj): + return obj.junior.auth.id + + @staticmethod + def get_user_type(obj): + return JUNIOR + + +class UserCSVReportSerializer(serializers.ModelSerializer): + """ + user csv/xls report serializer + """ + name = serializers.SerializerMethodField() + phone_number = serializers.SerializerMethodField() + user_type = serializers.SerializerMethodField() + is_active = serializers.SerializerMethodField() + date_joined = serializers.SerializerMethodField() + + class Meta: + """ + meta class + """ + model = USER + fields = ('name', 'email', 'phone_number', 'user_type', 'is_active', 'date_joined') + + @staticmethod + def get_name(obj): + """ + :param obj: user object + :return: full name + """ + return get_user_full_name(obj) + + @staticmethod + def get_phone_number(obj): + """ + :param obj: user object + :return: user phone number + """ + if profile := (obj.guardian_profile.all().first() or obj.junior_profile.all().first()): + return f"+{profile.country_code}{profile.phone}" \ + if profile.country_code and profile.phone else profile.phone + else: + return None + + @staticmethod + def get_user_type(obj): + """ + :param obj: user object + :return: user type + """ + if obj.guardian_profile.all().first(): + return dict(USER_TYPE).get('2').capitalize() + elif obj.junior_profile.all().first(): + return dict(USER_TYPE).get('1').capitalize() + else: + return None + + @staticmethod + def get_is_active(obj): + """ + :param obj: user object + :return: user type + """ + if profile := (obj.guardian_profile.all().first() or obj.junior_profile.all().first()): + return "Active" if profile.is_active else "Inactive" + + @staticmethod + def get_date_joined(obj): + """ + :param obj: user obj + :return: formatted date + """ + date = obj.date_joined.strftime("%d %b %Y") + return date diff --git a/web_admin/serializers/article_serializer.py b/web_admin/serializers/article_serializer.py index e125acf..9fa5651 100644 --- a/web_admin/serializers/article_serializer.py +++ b/web_admin/serializers/article_serializer.py @@ -10,6 +10,8 @@ from base.constants import (ARTICLE_SURVEY_POINTS, MAX_ARTICLE_CARD, MIN_ARTICLE # local imports from base.messages import ERROR_CODE from guardian.utils import upload_image_to_alibaba +from notifications.constants import NEW_ARTICLE_PUBLISHED +from notifications.utils import send_notification_multiple_user from web_admin.models import Article, ArticleCard, SurveyOption, ArticleSurvey, DefaultArticleCardImage from web_admin.utils import pop_id, get_image_url from junior.models import JuniorArticlePoints, JuniorArticle @@ -119,11 +121,15 @@ class ArticleSerializer(serializers.ModelSerializer): option = pop_id(option) SurveyOption.objects.create(survey=survey_obj, **option) + # All juniors will receive notification when admin add any new financial learnings/article + send_notification_multiple_user.delay(NEW_ARTICLE_PUBLISHED, None, None, {}) + return article def update(self, instance, validated_data): """ to update article and related table + :param validated_data: :param instance: article object, :return: article object """ @@ -219,8 +225,7 @@ class ArticleListSerializer(serializers.ModelSerializer): """ serializer for article API """ - article_cards = ArticleCardSerializer(many=True) - article_survey = ArticleSurveySerializer(many=True) + image = serializers.SerializerMethodField('get_image') total_points = serializers.SerializerMethodField('get_total_points') is_completed = serializers.SerializerMethodField('get_is_completed') @@ -229,12 +234,16 @@ class ArticleListSerializer(serializers.ModelSerializer): meta class """ model = Article - fields = ('id', 'title', 'description', 'article_cards', 'article_survey', 'total_points', 'is_completed') + fields = ('id', 'title', 'description','image', 'total_points', 'is_completed') + def get_image(self, obj): + """article image""" + if obj.article_cards.first(): + return obj.article_cards.first().image_url + return None def get_total_points(self, obj): """total points of article""" - total_question = ArticleSurvey.objects.filter(article=obj).count() - return total_question * NUMBER['five'] + return obj.article_survey.all().count() * NUMBER['five'] def get_is_completed(self, obj): """complete all question""" @@ -268,14 +277,14 @@ class ArticleQuestionSerializer(serializers.ModelSerializer): ans_obj = SurveyOption.objects.filter(survey=obj, is_answer=True).last() if ans_obj: return ans_obj.id - return str("None") + return None def get_attempted_answer(self, obj): """attempt question or not""" context_data = self.context.get('user') junior_article_obj = JuniorArticlePoints.objects.filter(junior__auth=context_data, - question=obj, is_answer_correct=True).last() - if junior_article_obj: + question=obj).last() + if junior_article_obj and junior_article_obj.submitted_answer: return junior_article_obj.submitted_answer.id return None diff --git a/web_admin/serializers/auth_serializer.py b/web_admin/serializers/auth_serializer.py index 712e284..bda89bd 100644 --- a/web_admin/serializers/auth_serializer.py +++ b/web_admin/serializers/auth_serializer.py @@ -14,7 +14,7 @@ from account.models import UserEmailOtp from base.constants import USER_TYPE from base.messages import ERROR_CODE from guardian.tasks import generate_otp -from base.tasks import send_email_otp +from base.tasks import send_email USER = get_user_model() @@ -48,11 +48,13 @@ class AdminOTPSerializer(serializers.ModelSerializer): :return: user_data """ email = validated_data['email'] - verification_code = generate_otp() - + template = 'email_reset_verification.email' # Send the verification code to the user's email - send_email_otp.delay(email, verification_code) + data = { + "verification_code": verification_code + } + send_email.delay([email], template, data) expiry = timezone.now() + timezone.timedelta(days=1) user_data, created = UserEmailOtp.objects.update_or_create(email=email, diff --git a/web_admin/serializers/user_management_serializer.py b/web_admin/serializers/user_management_serializer.py index 4bb0709..c8d0b1f 100644 --- a/web_admin/serializers/user_management_serializer.py +++ b/web_admin/serializers/user_management_serializer.py @@ -5,7 +5,8 @@ web_admin user_management serializers file from rest_framework import serializers from django.contrib.auth import get_user_model -from base.constants import USER_TYPE +from account.utils import get_user_full_name +from base.constants import USER_TYPE, GUARDIAN, JUNIOR # local imports from base.messages import ERROR_CODE, SUCCESS_CODE from guardian.models import Guardian @@ -37,7 +38,7 @@ class UserManagementListSerializer(serializers.ModelSerializer): :param obj: user object :return: full name """ - return f"{obj.first_name} {obj.last_name}" if obj.last_name else obj.first_name + return get_user_full_name(obj) @staticmethod def get_country_code(obj): @@ -108,7 +109,7 @@ class GuardianSerializer(serializers.ModelSerializer): """ model = Guardian fields = ('id', 'name', 'first_name', 'last_name', 'username', 'dob', 'gender', 'country_code', 'phone', - 'is_active', 'country_name', 'image', 'email') + 'is_active', 'country_name', 'image', 'email', 'is_deleted') def validate(self, attrs): """ @@ -144,7 +145,7 @@ class GuardianSerializer(serializers.ModelSerializer): :param obj: guardian object :return: full name """ - return f"{obj.user.first_name} {obj.user.last_name}" if obj.user.last_name else obj.user.first_name + return get_user_full_name(obj.user) @staticmethod def get_first_name(obj): @@ -187,7 +188,7 @@ class JuniorSerializer(serializers.ModelSerializer): """ model = Junior fields = ('id', 'name', 'first_name', 'last_name', 'username', 'dob', 'gender', 'country_code', 'phone', - 'is_active', 'country_name', 'image', 'email') + 'is_active', 'country_name', 'image', 'email', 'is_deleted') def validate(self, attrs): """ @@ -210,10 +211,10 @@ class JuniorSerializer(serializers.ModelSerializer): """ instance.auth.email = self.validated_data.get('email', instance.auth.email) instance.auth.username = self.validated_data.get('email', instance.auth.username) - instance.auth.save() + instance.auth.save(update_fields=['email', 'username']) instance.country_code = validated_data.get('country_code', instance.country_code) instance.phone = validated_data.get('phone', instance.phone) - instance.save() + instance.save(update_fields=['country_code', 'phone']) return instance @staticmethod @@ -222,7 +223,7 @@ class JuniorSerializer(serializers.ModelSerializer): :param obj: junior object :return: full name """ - return f"{obj.auth.first_name} {obj.auth.last_name}" if obj.auth.last_name else obj.auth.first_name + return get_user_full_name(obj.auth) @staticmethod def get_first_name(obj): @@ -265,33 +266,30 @@ class UserManagementDetailSerializer(serializers.ModelSerializer): model = USER fields = ('id', 'user_type', 'email', 'guardian_profile', 'junior_profile', 'associated_users') - @staticmethod - def get_user_type(obj): + def get_user_type(self, obj): """ :param obj: user object :return: user type """ - if obj.guardian_profile.all().first(): - return dict(USER_TYPE).get('2') - elif obj.junior_profile.all().first(): - return dict(USER_TYPE).get('1') - else: - return None + return GUARDIAN if self.context['user_type'] == GUARDIAN else JUNIOR - @staticmethod - def get_associated_users(obj): + def get_associated_users(self, obj): """ :param obj: user object :return: associated user """ - if profile := obj.guardian_profile.all().first(): + if self.context['user_type'] == GUARDIAN: + profile = obj.guardian_profile.all().only('user_id', 'guardian_code').first() if profile.guardian_code: - junior = Junior.objects.filter(guardian_code__contains=[profile.guardian_code], is_verified=True) + junior = Junior.objects.filter(guardian_code__contains=[profile.guardian_code], + is_verified=True).select_related('auth') serializer = JuniorSerializer(junior, many=True) return serializer.data - elif profile := obj.junior_profile.all().first(): + elif self.context['user_type'] == JUNIOR: + profile = obj.junior_profile.all().only('auth_id', 'guardian_code').first() if profile.guardian_code: - guardian = Guardian.objects.filter(guardian_code__in=profile.guardian_code, is_verified=True) + guardian = Guardian.objects.filter(guardian_code__in=profile.guardian_code, + is_verified=True).select_related('user') serializer = GuardianSerializer(guardian, many=True) return serializer.data else: diff --git a/web_admin/utils.py b/web_admin/utils.py index 9870b30..3dbb3b2 100644 --- a/web_admin/utils.py +++ b/web_admin/utils.py @@ -2,9 +2,10 @@ web_utils file """ import base64 +import datetime -from base.constants import ARTICLE_CARD_IMAGE_FOLDER -from guardian.utils import upload_image_to_alibaba +from base.constants import ARTICLE_CARD_IMAGE_FOLDER, DATE_FORMAT +from guardian.utils import upload_image_to_alibaba, upload_base64_image_to_alibaba def pop_id(data): @@ -29,10 +30,10 @@ def get_image_url(data): return data['image_url'] elif 'image_url' in data and type(data['image_url']) == str and data['image_url'].startswith('data:image'): base64_image = base64.b64decode(data.get('image_url').split(',')[1]) - image_name = f"{data['title']} {data.pop('image_name')}" if 'image_name' in data else data['title'] + image_name = data.pop('image_name') if 'image_name' in data else f"{data['title']}.jpg" filename = f"{ARTICLE_CARD_IMAGE_FOLDER}/{image_name}" # upload image on ali baba - image_url = upload_image_to_alibaba(base64_image, filename) + image_url = upload_base64_image_to_alibaba(base64_image, filename) return image_url elif 'image' in data and data['image'] is not None: image = data.pop('image') @@ -40,3 +41,21 @@ def get_image_url(data): # upload image on ali baba image_url = upload_image_to_alibaba(image, filename) return image_url + + +def get_dates(start_date, end_date): + """ + to get start and end date + :param start_date: format (yyyy-mm-dd) + :param end_date: format (yyyy-mm-dd) + :return: start and end date + """ + + if start_date and end_date: + start_date = datetime.datetime.strptime(start_date, DATE_FORMAT).date() + end_date = datetime.datetime.strptime(end_date, DATE_FORMAT).date() + else: + end_date = datetime.date.today() + start_date = end_date - datetime.timedelta(days=6) + + return start_date, end_date diff --git a/web_admin/views/analytics.py b/web_admin/views/analytics.py index 8c21cb3..926cd47 100644 --- a/web_admin/views/analytics.py +++ b/web_admin/views/analytics.py @@ -3,6 +3,9 @@ web_admin analytics view file """ # python imports import datetime +import io +import pandas as pd +import xlsxwriter # third party imports from rest_framework.viewsets import GenericViewSet @@ -16,15 +19,18 @@ from django.db.models import Count from django.db.models.functions import TruncDate from django.db.models import F, Window from django.db.models.functions.window import Rank +from django.http import HttpResponse # local imports -from account.utils import custom_response -from base.constants import PENDING, IN_PROGRESS, REJECTED, REQUESTED, COMPLETED, EXPIRED, DATE_FORMAT +from account.utils import custom_response, get_user_full_name +from base.constants import PENDING, IN_PROGRESS, REJECTED, REQUESTED, COMPLETED, EXPIRED, DATE_FORMAT, TASK_STATUS from guardian.models import JuniorTask +from guardian.utils import upload_excel_file_to_alibaba from junior.models import JuniorPoints from web_admin.pagination import CustomPageNumberPagination from web_admin.permission import AdminPermission -from web_admin.serializers.analytics_serializer import LeaderboardSerializer +from web_admin.serializers.analytics_serializer import LeaderboardSerializer, UserCSVReportSerializer +from web_admin.utils import get_dates USER = get_user_model() @@ -47,7 +53,7 @@ class AnalyticsViewSet(GenericViewSet): ).prefetch_related('guardian_profile', 'junior_profile' ).exclude(junior_profile__isnull=True, - guardian_profile__isnull=True).order_by('date_joined') + guardian_profile__isnull=True).order_by('-date_joined') return user_qs @action(methods=['get'], url_name='users-count', url_path='users-count', detail=False) @@ -58,13 +64,8 @@ class AnalyticsViewSet(GenericViewSet): :param request: end_date: date format (yyyy-mm-dd) :return: """ - - end_date = datetime.date.today() - start_date = end_date - datetime.timedelta(days=6) - - if request.query_params.get('start_date') and request.query_params.get('end_date'): - start_date = datetime.datetime.strptime(request.query_params.get('start_date'), DATE_FORMAT) - end_date = datetime.datetime.strptime(request.query_params.get('end_date'), DATE_FORMAT) + start_date, end_date = get_dates(request.query_params.get('start_date'), + request.query_params.get('end_date')) user_qs = self.get_queryset() queryset = user_qs.filter(date_joined__range=(start_date, (end_date + datetime.timedelta(days=1)))) @@ -83,12 +84,8 @@ class AnalyticsViewSet(GenericViewSet): :param request: end_date: date format (yyyy-mm-dd) :return: """ - end_date = datetime.date.today() - start_date = end_date - datetime.timedelta(days=6) - - if request.query_params.get('start_date') and request.query_params.get('end_date'): - start_date = datetime.datetime.strptime(request.query_params.get('start_date'), DATE_FORMAT) - end_date = datetime.datetime.strptime(request.query_params.get('end_date'), DATE_FORMAT) + start_date, end_date = get_dates(request.query_params.get('start_date'), + request.query_params.get('end_date')) user_qs = self.get_queryset() signup_data = user_qs.filter(date_joined__range=[start_date, (end_date + datetime.timedelta(days=1))] @@ -105,22 +102,20 @@ class AnalyticsViewSet(GenericViewSet): :param request: end_date: date format (yyyy-mm-dd) :return: """ - end_date = datetime.date.today() - start_date = end_date - datetime.timedelta(days=6) - - if request.query_params.get('start_date') and request.query_params.get('end_date'): - start_date = datetime.datetime.strptime(request.query_params.get('start_date'), DATE_FORMAT) - end_date = datetime.datetime.strptime(request.query_params.get('end_date'), DATE_FORMAT) + start_date, end_date = get_dates(request.query_params.get('start_date'), + request.query_params.get('end_date')) assign_tasks = JuniorTask.objects.filter( created_at__range=[start_date, (end_date + datetime.timedelta(days=1))] - ).exclude(task_status__in=[PENDING, EXPIRED]) + ) data = { 'task_completed': assign_tasks.filter(task_status=COMPLETED).count(), + 'task_pending': assign_tasks.filter(task_status=PENDING).count(), 'task_in_progress': assign_tasks.filter(task_status=IN_PROGRESS).count(), 'task_requested': assign_tasks.filter(task_status=REQUESTED).count(), 'task_rejected': assign_tasks.filter(task_status=REJECTED).count(), + 'task_expired': assign_tasks.filter(task_status=EXPIRED).count(), } return custom_response(None, data) @@ -133,11 +128,120 @@ class AnalyticsViewSet(GenericViewSet): :param request: :return: """ - queryset = JuniorPoints.objects.prefetch_related('junior', 'junior__auth').annotate(rank=Window( - expression=Rank(), - order_by=[F('total_points').desc(), 'junior__created_at'] - )).order_by('-total_points', 'junior__created_at') + queryset = JuniorPoints.objects.filter( + junior__is_verified=True + ).select_related('junior', 'junior__auth').annotate(rank=Window( + expression=Rank(), + order_by=[F('total_points').desc(), 'junior__created_at'] + )).order_by('-total_points', 'junior__created_at') paginator = CustomPageNumberPagination() paginated_queryset = paginator.paginate_queryset(queryset, request) serializer = self.serializer_class(paginated_queryset, many=True) return custom_response(None, serializer.data) + + @action(methods=['get'], url_name='export-excel', url_path='export-excel', detail=False) + def export_excel(self, request): + """ + to export users count, task details and top juniors in csv/excel file + :param request: start_date: date format (yyyy-mm-dd) + :param request: end_date: date format (yyyy-mm-dd) + :return: + """ + + response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + response['Content-Disposition'] = 'attachment; filename="ZOD_Bank_Analytics.xlsx"' + + start_date, end_date = get_dates(request.query_params.get('start_date'), + request.query_params.get('end_date')) + + # Use BytesIO for binary data + buffer = io.BytesIO() + + # Create an XlsxWriter Workbook object + workbook = xlsxwriter.Workbook(buffer) + + # Add sheets + sheets = ['Users', 'Assign Tasks', 'Juniors Leaderboard'] + + for sheet_name in sheets: + worksheet = workbook.add_worksheet(name=sheet_name) + + # sheet 1 for Total Users + if sheet_name == 'Users': + user_qs = self.get_queryset() + queryset = user_qs.filter(date_joined__range=(start_date, (end_date + datetime.timedelta(days=1)))) + serializer = UserCSVReportSerializer(queryset, many=True) + + df_users = pd.DataFrame([ + {'Name': user['name'], 'Email': user['email'], + 'Phone Number': user['phone_number'], 'User Type': user['user_type'], + 'Status': user['is_active'], 'Date Joined': user['date_joined']} + for user in serializer.data + ]) + write_excel_worksheet(worksheet, df_users) + + # sheet 2 for Assign Task + elif sheet_name == 'Assign Tasks': + assign_tasks = JuniorTask.objects.filter( + created_at__range=[start_date, (end_date + datetime.timedelta(days=1))] + ).select_related('junior__auth', 'guardian__user').only('task_name', 'task_status', + 'junior__auth__first_name', + 'junior__auth__last_name', + 'guardian__user__first_name', + 'guardian__user__last_name',) + + df_tasks = pd.DataFrame([ + {'Task Name': task.task_name, 'Assign To': get_user_full_name(task.junior.auth), + 'Assign By': get_user_full_name(task.guardian.user), + 'Task Status': dict(TASK_STATUS).get(task.task_status).capitalize()} + for task in assign_tasks + ]) + + write_excel_worksheet(worksheet, df_tasks) + + # sheet 3 for Juniors Leaderboard and rank + elif sheet_name == 'Juniors Leaderboard': + queryset = JuniorPoints.objects.filter( + junior__is_verified=True + ).select_related('junior', 'junior__auth').annotate(rank=Window( + expression=Rank(), + order_by=[F('total_points').desc(), 'junior__created_at'] + )).order_by('-total_points', 'junior__created_at')[:15] + df_leaderboard = pd.DataFrame([ + { + 'Name': get_user_full_name(junior.junior.auth), + 'Points': junior.total_points, + 'Rank': junior.rank + } + for junior in queryset + ]) + + write_excel_worksheet(worksheet, df_leaderboard) + + # Close the workbook to save the content + workbook.close() + + # Reset the buffer position and write the content to the response + buffer.seek(0) + response.write(buffer.getvalue()) + buffer.close() + + filename = f"{'analytics'}/{'ZOD_Bank_Analytics.xlsx'}" + file_link = upload_excel_file_to_alibaba(response, filename) + return custom_response(None, file_link) + + +def write_excel_worksheet(worksheet, dataframe): + """ + to perform write action on worksheets + :param worksheet: + :param dataframe: + :return: worksheet + """ + for idx, col in enumerate(dataframe.columns): + # Write header + worksheet.write(0, idx, col) + for row_num, row in enumerate(dataframe.values, start=1): + for col_num, value in enumerate(row): + worksheet.write(row_num, col_num, value) + return worksheet diff --git a/web_admin/views/article.py b/web_admin/views/article.py index 13c41c2..902f579 100644 --- a/web_admin/views/article.py +++ b/web_admin/views/article.py @@ -44,9 +44,20 @@ class ArticleViewSet(GenericViewSet, mixins.CreateModelMixin, mixins.UpdateModel def create(self, request, *args, **kwargs): """ article create api method - :param request: - :param args: - :param kwargs: + :param request: { "title": "string", "description": "string", + "article_cards": [ + { "title": "string", + "description": "string", + "image_name": "string", + "image_url": "string" + } ], + "article_survey": [ + { "question": "string", + "options": [ + { "option": "string", + "is_answer": true } + ] } + ] } :return: success message """ serializer = self.serializer_class(data=request.data) @@ -57,9 +68,24 @@ class ArticleViewSet(GenericViewSet, mixins.CreateModelMixin, mixins.UpdateModel def update(self, request, *args, **kwargs): """ article update api method - :param request: - :param args: - :param kwargs: + :param request: article_id, + { "title": "string", "description": "string", + "article_cards": [ + { "id": 0, + "title": "string", + "description": "string", + "image_name": "string", + "image_url": "string" + } ], + "article_survey": [ + { "id": 0, + "question": "string", + "options": [ + { "id": 0, + "option": "string", + "is_answer": true + } ] + } ] } :return: success message """ article = self.get_object() @@ -72,8 +98,6 @@ class ArticleViewSet(GenericViewSet, mixins.CreateModelMixin, mixins.UpdateModel """ article list api method :param request: - :param args: - :param kwargs: :return: list of article """ queryset = self.get_queryset() @@ -86,9 +110,7 @@ class ArticleViewSet(GenericViewSet, mixins.CreateModelMixin, mixins.UpdateModel def retrieve(self, request, *args, **kwargs): """ article detail api method - :param request: - :param args: - :param kwargs: + :param request: article_id :return: article detail data """ queryset = self.get_object() @@ -98,9 +120,7 @@ class ArticleViewSet(GenericViewSet, mixins.CreateModelMixin, mixins.UpdateModel def destroy(self, request, *args, **kwargs): """ article delete (soft delete) api method - :param request: - :param args: - :param kwargs: + :param request: article_id :return: success message """ article = self.get_object() @@ -177,7 +197,10 @@ class DefaultArticleCardImagesViewSet(GenericViewSet, mixins.CreateModelMixin, m def create(self, request, *args, **kwargs): """ api method to upload default article card images - :param request: + :param request: { + "image_name": "string", + "image": "image_file" + } :return: success message """ serializer = self.serializer_class(data=request.data) @@ -206,10 +229,7 @@ class ArticleListViewSet(GenericViewSet, mixins.ListModelMixin): http_method_names = ['get',] def get_queryset(self): - article = self.queryset.objects.filter(is_deleted=False).prefetch_related( - 'article_cards', 'article_survey', 'article_survey__options' - ).order_by('-created_at') - queryset = self.filter_queryset(article) + queryset = self.queryset.objects.filter(is_deleted=False, is_published=True).order_by('-created_at') return queryset def list(self, request, *args, **kwargs): @@ -225,7 +245,9 @@ class ArticleListViewSet(GenericViewSet, mixins.ListModelMixin): return custom_response(None, data=serializer.data) class ArticleCardListViewSet(viewsets.ModelViewSet): - """Junior Points viewset""" + """Article card list + use below query param + article_id""" serializer_class = ArticleCardlistSerializer permission_classes = [IsAuthenticated] http_method_names = ('get',) @@ -234,7 +256,9 @@ class ArticleCardListViewSet(viewsets.ModelViewSet): """get queryset""" return ArticleCard.objects.filter(article=self.request.GET.get('article_id')) def list(self, request, *args, **kwargs): - """profile view""" + """Article card list + use below query param + article_id""" try: queryset = self.get_queryset() diff --git a/web_admin/views/auth.py b/web_admin/views/auth.py index fae973e..73f19e5 100644 --- a/web_admin/views/auth.py +++ b/web_admin/views/auth.py @@ -27,6 +27,7 @@ class ForgotAndResetPasswordViewSet(GenericViewSet): def admin_otp(self, request): """ api method to send otp + :param request: {"email": "string"} :return: success message """ serializer = self.serializer_class(data=request.data) @@ -40,6 +41,7 @@ class ForgotAndResetPasswordViewSet(GenericViewSet): def admin_verify_otp(self, request): """ api method to verify otp + :param request: {"email": "string", "otp": "otp"} :return: success message """ serializer = self.serializer_class(data=request.data) @@ -52,6 +54,7 @@ class ForgotAndResetPasswordViewSet(GenericViewSet): def admin_create_password(self, request): """ api method to create new password + :param request: {"email": "string", "new_password": "string", "confirm_password": "string"} :return: success message """ serializer = self.serializer_class(data=request.data) diff --git a/web_admin/views/user_management.py b/web_admin/views/user_management.py index 8f53a73..6980a7a 100644 --- a/web_admin/views/user_management.py +++ b/web_admin/views/user_management.py @@ -12,8 +12,11 @@ from django.db.models import Q # local imports from account.utils import custom_response, custom_error_response -from base.constants import USER_TYPE +from base.constants import USER_TYPE, GUARDIAN, JUNIOR from base.messages import SUCCESS_CODE, ERROR_CODE +from base.tasks import send_email +from guardian.models import Guardian +from junior.models import Junior from web_admin.permission import AdminPermission from web_admin.serializers.user_management_serializer import (UserManagementListSerializer, UserManagementDetailSerializer, GuardianSerializer, @@ -70,12 +73,14 @@ class UserManagementViewSet(GenericViewSet, mixins.ListModelMixin, """ if self.request.query_params.get('user_type') not in [dict(USER_TYPE).get('1'), dict(USER_TYPE).get('2')]: return custom_error_response(ERROR_CODE['2067'], status.HTTP_400_BAD_REQUEST) + queryset = self.queryset - if self.request.query_params.get('user_type') == dict(USER_TYPE).get('2'): - queryset = queryset.filter(guardian_profile__user__id=kwargs['pk']) - elif self.request.query_params.get('user_type') == dict(USER_TYPE).get('1'): - queryset = queryset.filter(junior_profile__auth__id=kwargs['pk']) - serializer = UserManagementDetailSerializer(queryset, many=True) + queryset = queryset.filter(id=kwargs['pk']) + + serializer = UserManagementDetailSerializer( + queryset, many=True, + context={'user_type': self.request.query_params.get('user_type')} + ) return custom_response(None, data=serializer.data) def partial_update(self, request, *args, **kwargs): @@ -87,15 +92,16 @@ class UserManagementViewSet(GenericViewSet, mixins.ListModelMixin, """ if self.request.query_params.get('user_type') not in [dict(USER_TYPE).get('1'), dict(USER_TYPE).get('2')]: return custom_error_response(ERROR_CODE['2067'], status.HTTP_400_BAD_REQUEST) - queryset = self.queryset if self.request.query_params.get('user_type') == dict(USER_TYPE).get('2'): - user_obj = queryset.filter(guardian_profile__user__id=kwargs['pk']).first() - serializer = GuardianSerializer(user_obj.guardian_profile.all().first(), + guardian = Guardian.objects.filter(user_id=kwargs['pk'], is_verified=True + ).select_related('user').first() + serializer = GuardianSerializer(guardian, request.data, context={'user_id': kwargs['pk']}) elif self.request.query_params.get('user_type') == dict(USER_TYPE).get('1'): - user_obj = queryset.filter(junior_profile__auth__id=kwargs['pk']).first() - serializer = JuniorSerializer(user_obj.junior_profile.all().first(), + junior = Junior.objects.filter(auth_id=kwargs['pk'], is_verified=True + ).select_related('auth').first() + serializer = JuniorSerializer(junior, request.data, context={'user_id': kwargs['pk']}) serializer.is_valid(raise_exception=True) @@ -110,17 +116,21 @@ class UserManagementViewSet(GenericViewSet, mixins.ListModelMixin, user_type {'guardian' for Guardian, 'junior' for Junior} mandatory :return: success message """ - if self.request.query_params.get('user_type') not in [dict(USER_TYPE).get('1'), dict(USER_TYPE).get('2')]: + user_type = self.request.query_params.get('user_type') + if user_type not in [GUARDIAN, JUNIOR]: return custom_error_response(ERROR_CODE['2067'], status.HTTP_400_BAD_REQUEST) - queryset = self.queryset - if self.request.query_params.get('user_type') == dict(USER_TYPE).get('2'): - user_obj = queryset.filter(guardian_profile__user__id=kwargs['pk']).first() - obj = user_obj.guardian_profile.all().first() - obj.is_active = False if obj.is_active else True - obj.save() - elif self.request.query_params.get('user_type') == dict(USER_TYPE).get('1'): - user_obj = queryset.filter(junior_profile__auth__id=kwargs['pk']).first() - obj = user_obj.junior_profile.all().first() - obj.is_active = False if obj.is_active else True - obj.save() + + email_template = 'user_deactivate.email' + + if user_type == GUARDIAN: + obj = Guardian.objects.filter(user_id=kwargs['pk'], is_verified=True).select_related('user').first() + elif user_type == JUNIOR: + obj = Junior.objects.filter(auth_id=kwargs['pk'], is_verified=True).select_related('auth').first() + + if obj.is_active: + obj.is_active = False + send_email([obj.user.email if user_type == GUARDIAN else obj.auth.email], email_template) + else: + obj.is_active = True + obj.save() return custom_response(SUCCESS_CODE['3038']) diff --git a/zod_bank/celery.py b/zod_bank/celery.py index 5cc2829..039ea03 100644 --- a/zod_bank/celery.py +++ b/zod_bank/celery.py @@ -27,19 +27,24 @@ app.config_from_object('django.conf:settings') # Load task modules from all registered Django apps. app.autodiscover_tasks() +# scheduled task +app.conf.beat_schedule = { + "expired_task": { + "task": "guardian.utils.update_expired_task_status", + "schedule": crontab(minute=0, hour=0), + }, + 'notify_task_expiry': { + 'task': 'base.tasks.notify_task_expiry', + 'schedule': crontab(minute='0', hour='18'), + }, + 'notify_top_junior': { + 'task': 'base.tasks.notify_top_junior', + 'schedule': crontab(minute='0', hour='*/2'), + }, +} + @app.task(bind=True) def debug_task(self): """ celery debug task """ print(f'Request: {self.request!r}') - - -"""cron task""" - - -app.conf.beat_schedule = { - "expired_task": { - "task": "guardian.utils.update_expired_task_status", - "schedule": crontab(minute=0, hour=0), - }, -} diff --git a/zod_bank/settings.py b/zod_bank/settings.py index 2d003c1..781df80 100644 --- a/zod_bank/settings.py +++ b/zod_bank/settings.py @@ -125,6 +125,7 @@ SIMPLE_JWT = { # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases DATABASES = { + # default db setting 'default': { 'ENGINE': 'django.contrib.gis.db.backends.postgis', 'NAME':os.getenv('DB_NAME'), @@ -177,6 +178,9 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] +# database query logs settings +# Allows us to check db hits +# useful to optimize db query and hit LOGGING = { "version": 1, "filters": { @@ -193,6 +197,7 @@ LOGGING = { "class": "logging.StreamHandler" } }, + # database logger "loggers": { "django.db.backends": { "level": "DEBUG", @@ -242,6 +247,7 @@ CORS_ALLOW_HEADERS = ( 'x-requested-with', ) +# CORS header settings CORS_EXPOSE_HEADERS = ( 'Access-Control-Allow-Origin: *', ) @@ -297,5 +303,7 @@ STATIC_URL = 'static/' # define static root STATIC_ROOT = 'static' +# media url MEDIA_URL = "/media/" +# media path MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), 'media')