diff --git a/account/custom_middleware.py b/account/custom_middleware.py index 7a06e43..c2125cd 100644 --- a/account/custom_middleware.py +++ b/account/custom_middleware.py @@ -7,7 +7,9 @@ from rest_framework.renderers import JSONRenderer from account.utils import custom_error_response from account.models import UserDeviceDetails 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,16 @@ from base.messages import ERROR_CODE, SUCCESS_CODE # multiple devices only # user can login in single # device at a time""" + +def custom_response(custom_error): + """custom response""" + 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) + return response class CustomMiddleware(object): """Custom middleware""" def __init__(self, get_response): @@ -26,15 +38,22 @@ 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') + api_endpoint = request.path if request.user.is_authenticated: - """device details""" + # device details device_details = UserDeviceDetails.objects.filter(user=request.user, device_id=device_id).last() - if device_id and not device_details: + 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 device_id and 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 = 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) + response = custom_response(custom_error) return response diff --git a/account/serializers.py b/account/serializers.py index ebdc263..e946d15 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -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): + """ + + """ 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,7 +300,8 @@ 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""" 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/utils.py b/account/utils.py index 52c016a..cbdab1e 100644 --- a/account/utils.py +++ b/account/utils.py @@ -95,6 +95,7 @@ def junior_account_update(user_tb): junior_data.is_verified = False junior_data.guardian_code = '{}' junior_data.guardian_code_status = str(NUMBER['one']) + 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): @@ -251,3 +277,9 @@ 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.last_name else user_obj.first_name diff --git a/account/views.py b/account/views.py index 2d8149d..0c86aea 100644 --- a/account/views.py +++ b/account/views.py @@ -39,7 +39,7 @@ 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 @@ -193,15 +193,30 @@ 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 + """ + 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""" @@ -213,40 +228,40 @@ 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 = timezone.now() + timezone.timedelta(days=1) - 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) + + """ + Post method to validate serializer + """ + 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 +302,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 +310,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 +338,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) diff --git a/base/constants.py b/base/constants.py index 9688c0f..d376971 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" diff --git a/base/messages.py b/base/messages.py index 10fbfaa..d87b48e 100644 --- a/base/messages.py +++ b/base/messages.py @@ -96,8 +96,15 @@ 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", + "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", } """Success message code""" @@ -163,6 +170,8 @@ SUCCESS_CODE = { "3043": "Read article card successfully", # remove guardian code request "3044": "Remove guardian code request successfully", + # create faq + "3045": "Create FAQ data" } """status code error""" diff --git a/celerybeat-schedule b/celerybeat-schedule index f2510fc..cc8251c 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..8db860b 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 @@ -29,8 +30,9 @@ from .utils import real_time, convert_timedelta_into_datetime, update_referral_p # notification's constant from notifications.constants import TASK_POINTS, TASK_REJECTED # send notification function -from notifications.utils import send_notification - +from notifications.utils import send_notification, send_notification_to_junior +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ # In this serializer file # define user serializer, @@ -42,10 +44,45 @@ from notifications.utils import send_notification # guardian profile serializer, # approve junior serializer, # approve task serializer, +from rest_framework import serializers + +class PasswordValidator: + 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): + 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: + 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): + raise serializers.ValidationError(_("Password must contain at least one uppercase letter.")) + + if self.require_numbers and not any(char.isdigit() for char in password): + 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): + 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 +251,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""" @@ -340,7 +377,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): @@ -383,7 +420,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_to_junior.delay(TASK_POINTS, None, junior_details.auth.id, + {'task_id': instance.id}) else: # reject the task instance.task_status = str(NUMBER['three']) @@ -391,7 +429,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_to_junior.delay(TASK_REJECTED, None, junior_details.auth.id, + {'task_id': instance.id}) instance.save() junior_data.save() return junior_details 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/utils.py b/guardian/utils.py index 57e8080..14bd36a 100644 --- a/guardian/utils.py +++ b/guardian/utils.py @@ -21,7 +21,9 @@ from zod_bank.celery import app # notification's constant from notifications.constants import REFERRAL_POINTS # send notification function -from notifications.utils import send_notification +from notifications.utils import send_notification, send_notification_to_junior + + # 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_to_junior.delay(REFERRAL_POINTS, None, junior_queryset.auth.id, {}) diff --git a/guardian/views.py b/guardian/views.py index 88f567c..538ed4c 100644 --- a/guardian/views.py +++ b/guardian/views.py @@ -38,8 +38,8 @@ from account.utils import custom_response, custom_error_response, OTP_EXPIRY, se from base.messages import ERROR_CODE, SUCCESS_CODE from base.constants import NUMBER, GUARDIAN_CODE_STATUS from .utils import upload_image_to_alibaba -from notifications.constants import REGISTRATION, TASK_CREATED, LEADERBOARD_RANKING -from notifications.utils import send_notification +from notifications.constants import REGISTRATION, TASK_ASSIGNED, LEADERBOARD_RANKING +from notifications.utils import send_notification_to_junior """ Define APIs """ # Define Signup API, @@ -62,7 +62,7 @@ class SignupViewset(viewsets.ModelViewSet): 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(): - user = serializer.save() + serializer.save() """Generate otp""" otp = generate_otp() # expire otp after 1 day @@ -72,8 +72,6 @@ class SignupViewset(viewsets.ModelViewSet): 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, {}) 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) @@ -81,7 +79,9 @@ class SignupViewset(viewsets.ModelViewSet): return custom_error_response(ERROR_CODE['2028'], response_status=status.HTTP_400_BAD_REQUEST) class UpdateGuardianProfile(viewsets.ViewSet): - """Update guardian profile""" + """ + Update guardian profile + """ serializer_class = CreateGuardianSerializer permission_classes = [IsAuthenticated] @@ -166,6 +166,10 @@ class CreateTaskAPIView(viewsets.ModelViewSet): try: image = request.data['default_image'] junior = request.data['junior'] + junior_id = Junior.objects.filter(id=junior).last() + guardian_data = Guardian.objects.filter(user=request.user).last() + if guardian_data.guardian_code in junior_id.guardian_code: + 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) @@ -186,9 +190,10 @@ class CreateTaskAPIView(viewsets.ModelViewSet): 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, {}) + task = serializer.save() + + send_notification_to_junior.delay(TASK_ASSIGNED, request.auth.payload['user_id'], + 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) except Exception as e: @@ -241,7 +246,7 @@ class TopJuniorListAPIView(viewsets.ModelViewSet): # 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, {}) + # send_notification_to_junior.delay(LEADERBOARD_RANKING, None, junior.junior.auth.id, {}) junior.save() serializer = self.get_serializer(junior_total_points[:NUMBER['fifteen']], many=True) return custom_response(None, serializer.data, response_status=status.HTTP_200_OK) @@ -254,31 +259,29 @@ class ApproveJuniorAPIView(viewsets.ViewSet): 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 def create(self, request, *args, **kwargs): """ junior list""" 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() 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() + junior_queryset.guardian_code = None + junior_queryset.guardian_code_status = str(NUMBER['one']) + junior_queryset.save() 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) @@ -289,34 +292,31 @@ class ApproveTaskAPIView(viewsets.ViewSet): 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 - def create(self, request, *args, **kwargs): """ junior list""" # 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) 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/models.py b/junior/models.py index 025843e..773557b 100644 --- a/junior/models.py +++ b/junior/models.py @@ -68,6 +68,8 @@ 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""" @@ -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..b4aa54f 100644 --- a/junior/serializers.py +++ b/junior/serializers.py @@ -12,7 +12,7 @@ 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, @@ -21,7 +21,7 @@ from guardian.models import Guardian, JuniorTask from account.models import UserEmailOtp, UserNotification from junior.utils import junior_notification_email, junior_approval_mail from guardian.utils import real_time, update_referral_points, convert_timedelta_into_datetime -from notifications.utils import send_notification +from notifications.utils import send_notification, send_notification_to_junior, send_notification_to_guardian from notifications.constants import (INVITED_GUARDIAN, APPROVED_JUNIOR, SKIPPED_PROFILE_SETUP, TASK_ACTION, TASK_SUBMITTED) @@ -98,7 +98,7 @@ class CreateJuniorSerializer(serializers.ModelSerializer): 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_to_guardian.delay(APPROVED_JUNIOR, junior.auth.id, guardian_data.user.id, {}) 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 +147,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""" @@ -214,7 +214,8 @@ class JuniorDetailListSerializer(serializers.ModelSerializer): 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 +258,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""" @@ -305,7 +306,7 @@ class AddJuniorSerializer(serializers.ModelSerializer): """Notification email""" junior_notification_email(email, full_name, email, password) # push notification - send_notification.delay(SKIPPED_PROFILE_SETUP, None, junior_data.auth.id, {}) + send_notification_to_junior.delay(SKIPPED_PROFILE_SETUP, None, junior_data.auth.id, {}) return junior_data @@ -337,8 +338,10 @@ 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_to_junior.delay(TASK_SUBMITTED, instance.guardian.user.id, + instance.junior.auth.id, {'task_id': instance.id}) + send_notification_to_guardian.delay(TASK_ACTION, instance.junior.auth.id, + instance.guardian.user.id, {'task_id': instance.id}) return instance class JuniorPointsSerializer(serializers.ModelSerializer): @@ -393,7 +396,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""" @@ -448,8 +451,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, {}) + send_notification_to_junior.delay(INVITED_GUARDIAN, guardian_data.user.id, junior_data.auth.id, {}) + send_notification_to_guardian.delay(APPROVED_JUNIOR, junior_data.auth.id, guardian_data.user.id, {}) return guardian_data class StartTaskSerializer(serializers.ModelSerializer): @@ -506,3 +509,12 @@ class RemoveGuardianCodeSerializer(serializers.ModelSerializer): instance.guardian_code_status = str(NUMBER['one']) instance.save() return instance + +class FAQSerializer(serializers.ModelSerializer): + # FAQ Serializer + + class Meta(object): + # meta info + model = FAQ + fields = ('id', 'question', 'description') + diff --git a/junior/urls.py b/junior/urls.py index b145d4f..08fe33a 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)), diff --git a/junior/views.py b/junior/views.py index a86083d..8df8a09 100644 --- a/junior/views.py +++ b/junior/views.py @@ -10,6 +10,8 @@ from django.db.models import F import datetime import requests + +from rest_framework.viewsets import GenericViewSet, mixins """Django app import""" # Import guardian's model, @@ -30,19 +32,19 @@ 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) 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 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.utils import send_notification, send_notification_to_junior from notifications.constants import REMOVE_JUNIOR from web_admin.models import Article, ArticleSurvey, SurveyOption, ArticleCard from web_admin.serializers.article_serializer import (ArticleSerializer, ArticleListSerializer, @@ -169,7 +171,11 @@ 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) return custom_response(SUCCESS_CODE['3021'], response_status=status.HTTP_200_OK) # use AddJuniorSerializer serializer serializer = AddJuniorSerializer(data=request.data, context=info_data) @@ -182,9 +188,16 @@ 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] + if not junior: + return none + if guardian.guardian_code in junior.guardian_code: + return False + if type(junior.guardian_code) is list: + junior.guardian_code.append(guardian.guardian_code) + else: + junior.guardian_code = [guardian.guardian_code] junior.guardian_code_status = str(NUMBER['two']) junior.save() JuniorGuardianRelationship.objects.get_or_create(guardian=guardian, junior=junior, @@ -269,7 +282,7 @@ class RemoveJuniorAPIView(views.APIView): if serializer.is_valid(): # save serializer serializer.save() - send_notification.delay(REMOVE_JUNIOR, None, junior_queryset.auth.id, {}) + send_notification_to_junior.delay(REMOVE_JUNIOR, 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: @@ -334,8 +347,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 """ @@ -649,3 +665,49 @@ class RemoveGuardianCodeAPIView(views.APIView): 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 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: + :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/constants.py b/notifications/constants.py index b861142..59c2328 100644 --- a/notifications/constants.py +++ b/notifications/constants.py @@ -3,7 +3,7 @@ notification constants file """ from base.constants import NUMBER REGISTRATION = NUMBER['one'] -TASK_CREATED = NUMBER['two'] +TASK_ASSIGNED = NUMBER['two'] INVITED_GUARDIAN = NUMBER['three'] APPROVED_JUNIOR = NUMBER['four'] REFERRAL_POINTS = NUMBER['five'] @@ -21,17 +21,17 @@ 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." + TASK_ASSIGNED: { + "title": "New task assigned !!", + "body": "{from_user} has assigned you a new task." }, INVITED_GUARDIAN: { "title": "Invite guardian", "body": "Invite guardian successfully" }, APPROVED_JUNIOR: { - "title": "Approve junior", - "body": "You have request from junior to associate with you" + "title": "Approve junior !", + "body": "You have request from {from_user} to associate with you." }, REFERRAL_POINTS: { "title": "Earn Referral points", @@ -47,15 +47,15 @@ NOTIFICATION_DICT = { }, SKIPPED_PROFILE_SETUP: { "title": "Skipped profile setup!", - "body": "Your guardian has been setup your profile." + "body": "Your guardian {from_user} has setup your profile." }, TASK_SUBMITTED: { "title": "Task submitted!", "body": "Your task has been submitted successfully." }, TASK_ACTION: { - "title": "Task approval!", - "body": "You have request for task approval." + "title": "Task completion approval!", + "body": "You have request from {from_user} for task approval." }, LEADERBOARD_RANKING: { "title": "Leader board rank!", @@ -67,6 +67,6 @@ NOTIFICATION_DICT = { }, 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..14f1b20 100644 --- a/notifications/serializers.py +++ b/notifications/serializers.py @@ -35,7 +35,8 @@ 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""" diff --git a/notifications/utils.py b/notifications/utils.py index ba980e6..50dee81 100644 --- a/notifications/utils.py +++ b/notifications/utils.py @@ -10,6 +10,9 @@ from firebase_admin.messaging import Message, Notification as FirebaseNotificati from django.contrib.auth import get_user_model from account.models import UserNotification +from account.utils import get_user_full_name +from guardian.models import Guardian +from junior.models import Junior from notifications.constants import NOTIFICATION_DICT from notifications.models import Notification @@ -39,30 +42,80 @@ 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] +def get_basic_detail(from_user_id, to_user_id): + """ + used to get the basic details + """ 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 + return 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 get_notification_data(notification_type, from_user, to_user, extra_data): + """ + get notification and push data + :param notification_type: notification_type + :param from_user: from_user obj + :param to_user: 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() + notification_data['to_user'] = get_user_full_name(to_user) + if from_user: + from_user_name = get_user_full_name(from_user) + push_data['body'] = push_data['body'].format(from_user=from_user_name) + notification_data['body'] = notification_data['body'].format(from_user=from_user_name) + notification_data['from_user'] = from_user_name + + notification_data.update(extra_data) + + return notification_data, push_data + + +def send_notification(notification_type, from_user, to_user, extra_data): + """ + used to send the push for the given notification type + """ + # (from_user, to_user) = get_basic_detail(from_user_id, to_user_id) + notification_data, push_data = get_notification_data(notification_type, from_user, to_user, 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) + notification_to=to_user, data=notification_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) + 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) ) + + +@shared_task() +def send_notification_to_guardian(notification_type, from_user_id, to_user_id, extra_data): + from_user = None + if from_user_id: + from_user = Junior.objects.filter(auth_id=from_user_id).select_related('auth').first() + extra_data['from_user_image'] = from_user.image + from_user = from_user.auth + to_user = Guardian.objects.filter(user_id=to_user_id).select_related('user').first() + extra_data['to_user_image'] = to_user.image + send_notification(notification_type, from_user, to_user.user, extra_data) + + +@shared_task() +def send_notification_to_junior(notification_type, from_user_id, to_user_id, extra_data): + from_user = None + if from_user_id: + from_user = Guardian.objects.filter(user_id=from_user_id).select_related('user').first() + extra_data['from_user_image'] = from_user.image + from_user = from_user.user + to_user = Junior.objects.filter(auth_id=to_user_id).select_related('auth').first() + extra_data['to_user_image'] = to_user.image + send_notification(notification_type, from_user, to_user.auth, extra_data) + + diff --git a/notifications/views.py b/notifications/views.py index c66d655..5adb536 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -13,7 +13,7 @@ from base.messages import SUCCESS_CODE, ERROR_CODE from notifications.constants import TEST_NOTIFICATION # Import serializer from notifications.serializers import RegisterDevice, NotificationListSerializer, ReadNotificationSerializer -from notifications.utils import send_notification +from notifications.utils import send_notification, send_notification_to_guardian, send_notification_to_junior # Import model from notifications.models import Notification @@ -56,7 +56,10 @@ class NotificationViewSet(viewsets.GenericViewSet): to send test notification :return: """ - send_notification.delay(TEST_NOTIFICATION, None, request.auth.payload['user_id'], {}) + send_notification_to_guardian.delay(TEST_NOTIFICATION, None, request.auth.payload['user_id'], + {'task_id': None}) + send_notification_to_junior.delay(TEST_NOTIFICATION, None, request.auth.payload['user_id'], + {'task_id': None}) return custom_response(SUCCESS_CODE["3000"]) @action(methods=['get'], detail=False, url_path='list', url_name='list', 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..0e3418a 100644 --- a/web_admin/serializers/analytics_serializer.py +++ b/web_admin/serializers/analytics_serializer.py @@ -1,12 +1,24 @@ """ web_admin analytics serializer file """ +# third party imports from rest_framework import serializers +# django imports +from django.contrib.auth import get_user_model + +# local imports +from base.constants import USER_TYPE + 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 +28,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): @@ -44,9 +56,75 @@ class JuniorLeaderboardSerializer(serializers.ModelSerializer): class LeaderboardSerializer(serializers.ModelSerializer): + """ + leaderboard serializer + """ junior = JuniorLeaderboardSerializer() rank = serializers.IntegerField() class Meta: + """ + meta class + """ model = JuniorPoints fields = ('total_points', 'rank', 'junior') + + +class UserCSVReportSerializer(serializers.ModelSerializer): + """ + user csv/xls report serializer + """ + phone_number = serializers.SerializerMethodField() + user_type = serializers.SerializerMethodField() + is_active = serializers.SerializerMethodField() + date_joined = serializers.SerializerMethodField() + + class Meta: + """ + meta class + """ + model = USER + fields = ('first_name', 'last_name', 'email', 'phone_number', 'user_type', 'is_active', 'date_joined') + + @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/user_management_serializer.py b/web_admin/serializers/user_management_serializer.py index 4bb0709..21a0a3b 100644 --- a/web_admin/serializers/user_management_serializer.py +++ b/web_admin/serializers/user_management_serializer.py @@ -5,7 +5,7 @@ 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 base.constants import USER_TYPE, GUARDIAN, JUNIOR # local imports from base.messages import ERROR_CODE, SUCCESS_CODE from guardian.models import Guardian @@ -108,7 +108,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): """ @@ -187,7 +187,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 +210,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 @@ -265,33 +265,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..4170cf9 100644 --- a/web_admin/utils.py +++ b/web_admin/utils.py @@ -4,7 +4,7 @@ web_utils file import base64 from base.constants import ARTICLE_CARD_IMAGE_FOLDER -from guardian.utils import upload_image_to_alibaba +from guardian.utils import upload_image_to_alibaba, upload_base64_image_to_alibaba def pop_id(data): @@ -29,10 +29,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') diff --git a/web_admin/views/analytics.py b/web_admin/views/analytics.py index 8c21cb3..ddc4e0d 100644 --- a/web_admin/views/analytics.py +++ b/web_admin/views/analytics.py @@ -3,6 +3,12 @@ web_admin analytics view file """ # python imports import datetime +import io +import pandas as pd +import xlsxwriter +import tempfile +import oss2 +from django.conf import settings # third party imports from rest_framework.viewsets import GenericViewSet @@ -16,15 +22,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 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.serializers.user_management_serializer import UserManagementListSerializer USER = get_user_model() @@ -141,3 +150,108 @@ class AnalyticsViewSet(GenericViewSet): 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"' + + 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) + + # 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': f"{user['first_name']} {user['last_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 + ]) + for idx, col in enumerate(df_users.columns): + # Write header + worksheet.write(0, idx, col) + for row_num, row in enumerate(df_users.values, start=1): + for col_num, value in enumerate(row): + worksheet.write(row_num, col_num, value) + + # 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))] + ).exclude(task_status__in=[PENDING, EXPIRED]) + + df_tasks = pd.DataFrame([ + {'Task Name': task.task_name, 'Task Status': dict(TASK_STATUS).get(task.task_status).capitalize()} + for task in assign_tasks + ]) + + for idx, col in enumerate(df_tasks.columns): + # Write header + worksheet.write(0, idx, col) + for row_num, row in enumerate(df_tasks.values, start=1): + for col_num, value in enumerate(row): + worksheet.write(row_num, col_num, value) + + # sheet 3 for Juniors Leaderboard and rank + elif sheet_name == 'Juniors leaderboard': + 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')[:15] + df_leaderboard = pd.DataFrame([ + { + 'Junior Name': f"{junior.junior.auth.first_name} {junior.junior.auth.last_name}" + if junior.junior.auth.last_name else junior.junior.auth.first_name, + 'Points': junior.total_points, + 'Rank': junior.rank + } + for junior in queryset + ]) + + for idx, col in enumerate(df_leaderboard.columns): + # Write header + worksheet.write(0, idx, col) + for row_num, row in enumerate(df_leaderboard.values, start=1): + for col_num, value in enumerate(row): + worksheet.write(row_num, col_num, value) + + # 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) + diff --git a/web_admin/views/article.py b/web_admin/views/article.py index 13c41c2..49f76fa 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) 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..ecc9771 100644 --- a/web_admin/views/user_management.py +++ b/web_admin/views/user_management.py @@ -14,6 +14,8 @@ from django.db.models import Q from account.utils import custom_response, custom_error_response from base.constants import USER_TYPE from base.messages import SUCCESS_CODE, ERROR_CODE +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 +72,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 +91,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'): - 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).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) @@ -114,12 +117,12 @@ class UserManagementViewSet(GenericViewSet, mixins.ListModelMixin, 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() + user_obj = queryset.filter(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() + user_obj = queryset.filter(id=kwargs['pk']).first() obj = user_obj.junior_profile.all().first() obj.is_active = False if obj.is_active else True obj.save() 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')