diff --git a/account/custom_middleware.py b/account/custom_middleware.py index 42a3b0f..b3cc750 100644 --- a/account/custom_middleware.py +++ b/account/custom_middleware.py @@ -17,6 +17,8 @@ from guardian.models import Guardian # 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""" diff --git a/account/serializers.py b/account/serializers.py index dbb52a0..b783efd 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -123,7 +123,7 @@ class ChangePasswordSerializer(serializers.Serializer): def create(self, validated_data): """ - + change password """ new_password = validated_data.pop('new_password') current_password = validated_data.pop('current_password') @@ -392,7 +392,8 @@ class UserPhoneOtpSerializer(serializers.ModelSerializer): fields = '__all__' class ForceUpdateSerializer(serializers.ModelSerializer): - # ForceUpdate Serializer + """ ForceUpdate Serializer + """ class Meta(object): """ meta info """ diff --git a/base/messages.py b/base/messages.py index 9afbe4f..b4abf7d 100644 --- a/base/messages.py +++ b/base/messages.py @@ -101,13 +101,17 @@ ERROR_CODE = { "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": "You can not add more than 3 guardian" + "2081": "You can not add more than 3 guardian", + # guardian code not exist + "2082": "Guardian code does not exist" } """Success message code""" diff --git a/base/tasks.py b/base/tasks.py index 98d0df0..65dff3b 100644 --- a/base/tasks.py +++ b/base/tasks.py @@ -68,7 +68,7 @@ def notify_top_junior(): task to send notification for top leaderboard junior to all junior's :return: """ - junior_points_qs = JuniorPoints.objects.prefetch_related('junior', 'junior__auth').annotate(rank=Window( + junior_points_qs = JuniorPoints.objects.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') @@ -76,9 +76,8 @@ def notify_top_junior(): 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: - to_user_list = [junior_point.junior.auth for junior_point in junior_points_qs] send_notification_multiple_user(TOP_JUNIOR, new_top_position.junior.auth.id, JUNIOR, - to_user_list, {'points': new_top_position.total_points}) + {'points': new_top_position.total_points}) for junior_point in junior_points_qs: junior_point.position = junior_point.rank junior_point.save() diff --git a/celerybeat-schedule b/celerybeat-schedule index 4ebacd4..4c54db2 100644 Binary files a/celerybeat-schedule and b/celerybeat-schedule differ diff --git a/guardian/serializers.py b/guardian/serializers.py index 7146039..76b99b5 100644 --- a/guardian/serializers.py +++ b/guardian/serializers.py @@ -36,6 +36,7 @@ from django.utils.translation import gettext as _ # In this serializer file # define user serializer, +# define password validation # create guardian serializer, # task serializer, # guardian serializer, @@ -47,6 +48,7 @@ from django.utils.translation import gettext as _ 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 @@ -57,6 +59,7 @@ class PasswordValidator: 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( @@ -64,16 +67,20 @@ class PasswordValidator: ) 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.")) diff --git a/guardian/utils.py b/guardian/utils.py index 80cbc56..1d40c34 100644 --- a/guardian/utils.py +++ b/guardian/utils.py @@ -127,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 c755e87..c565201 100644 --- a/guardian/views.py +++ b/guardian/views.py @@ -255,7 +255,7 @@ class TopJuniorListAPIView(viewsets.ModelViewSet): return context def get_queryset(self): - queryset = JuniorPoints.objects.prefetch_related('junior', 'junior__auth').annotate(rank=Window( + queryset = JuniorPoints.objects.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') @@ -292,7 +292,8 @@ class ApproveJuniorAPIView(viewsets.ModelViewSet): if request.data['action'] == '1': # use ApproveJuniorSerializer serializer serializer = ApproveJuniorSerializer(context={"guardian_code": guardian.guardian_code, - "junior": junior_queryset, "action": request.data['action']}, + "junior": junior_queryset, + "action": request.data['action']}, data=request.data) if serializer.is_valid(): # save serializer diff --git a/junior/serializers.py b/junior/serializers.py index f85af2d..6ba4218 100644 --- a/junior/serializers.py +++ b/junior/serializers.py @@ -504,24 +504,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: + instance.guardian_code.remove(guardian_code) + else: + raise serializers.ValidationError({"error":ERROR_CODE['2082'],"code":"400", "status":"failed"}) + if not instance.guardian_code: + instance.guardian_code_status = str(NUMBER['one']) + elif instance.guardian_code and (len(instance.guardian_code) == 1 and '-' in instance.guardian_code): + instance.guardian_code_status = str(NUMBER['one']) + else: + instance.guardian_code_status = str(NUMBER['two']) instance.save() return instance class FAQSerializer(serializers.ModelSerializer): - # FAQ Serializer + """FAQ Serializer""" class Meta(object): - # meta info + """meta info""" model = FAQ fields = ('id', 'question', 'description') class CreateArticleCardSerializer(serializers.ModelSerializer): - # Article card Serializer + """Article card Serializer""" class Meta(object): - # meta info + """meta info""" model = ArticleCard fields = ('id', 'article') diff --git a/junior/utils.py b/junior/utils.py index dacb426..7533017 100644 --- a/junior/utils.py +++ b/junior/utils.py @@ -70,7 +70,7 @@ def get_junior_leaderboard_rank(junior_obj): :param junior_obj: :return: junior's position/rank """ - queryset = JuniorPoints.objects.prefetch_related('junior', 'junior__auth').annotate(rank=Window( + queryset = JuniorPoints.objects.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') diff --git a/junior/views.py b/junior/views.py index 72db293..b4a9d1d 100644 --- a/junior/views.py +++ b/junior/views.py @@ -207,13 +207,15 @@ class AddJuniorAPIView(viewsets.ModelViewSet): def associate_guardian(self, user): junior = Junior.objects.filter(auth__email=self.request.data['email']).first() guardian = Guardian.objects.filter(user=self.request.user).first() + 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) < 4: + if type(junior.guardian_code) is list and len(junior.guardian_code) < 3: junior.guardian_code.append(guardian.guardian_code) else: return "Max" @@ -264,10 +266,10 @@ class FilterJuniorAPIView(viewsets.ModelViewSet): manual_parameters=[ # Example of a query parameter openapi.Parameter( - 'title', # Query parameter name - openapi.IN_QUERY, # Parameter location + 'title', + openapi.IN_QUERY, description='title of the name', - type=openapi.TYPE_STRING, # Parameter type + type=openapi.TYPE_STRING, ), # Add more parameters as needed ] @@ -724,16 +726,21 @@ class CreateArticleCardAPIView(viewsets.ModelViewSet): class RemoveGuardianCodeAPIView(views.APIView): """Remove guardian code request API - No Payload""" + 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") 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() @@ -743,7 +750,8 @@ class RemoveGuardianCodeAPIView(views.APIView): # task in another state 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) + 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, diff --git a/notifications/constants.py b/notifications/constants.py index e2ae430..dc6ea75 100644 --- a/notifications/constants.py +++ b/notifications/constants.py @@ -14,10 +14,10 @@ TASK_REJECTED = 10 TASK_APPROVED = 11 PENDING_TASK_EXPIRING = 12 IN_PROGRESS_TASK_EXPIRING = 13 - TOP_JUNIOR = 14 -REMOVE_JUNIOR = 15 +NEW_ARTICLE_PUBLISHED = 15 +REMOVE_JUNIOR = 16 TEST_NOTIFICATION = 99 @@ -68,7 +68,8 @@ NOTIFICATION_DICT = { "title": "Task completion approval!", "body": "{from_user} completed her task {task_name}." }, - # Juniors will receive notification as soon as their task is rejected by custodians + # 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}." @@ -92,11 +93,18 @@ NOTIFICATION_DICT = { "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 + # 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 as soon as their custodians remove them from account REMOVE_JUNIOR: { "title": "Disassociate by guardian!", diff --git a/notifications/utils.py b/notifications/utils.py index ed7e350..a2759e0 100644 --- a/notifications/utils.py +++ b/notifications/utils.py @@ -8,6 +8,7 @@ from firebase_admin.messaging import Message, Notification as FirebaseNotificati # django imports from django.contrib.auth import get_user_model +from django.db.models import Q # local imports from account.models import UserNotification @@ -79,6 +80,7 @@ def get_notification_data(notification_type, from_user_id, from_user_type, to_us 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) @@ -124,10 +126,13 @@ def send_multiple_push(queryset, data): @shared_task() def send_notification_multiple_user(notification_type, from_user_id, from_user_type, - to_user_list: list = [], extra_data: dict = {}): + 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) + push_data = NOTIFICATION_DICT[notification_type].copy() notification_data = push_data.copy() points = extra_data.get('points', None) @@ -144,7 +149,7 @@ def send_notification_multiple_user(notification_type, from_user_id, from_user_t notification_from=from_user, data=notification_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) diff --git a/web_admin/serializers/article_serializer.py b/web_admin/serializers/article_serializer.py index 4e98e55..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 """ diff --git a/zod_bank/celery.py b/zod_bank/celery.py index 571b1e0..cf71dc8 100644 --- a/zod_bank/celery.py +++ b/zod_bank/celery.py @@ -27,6 +27,7 @@ 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", @@ -34,11 +35,11 @@ app.conf.beat_schedule = { }, 'notify_task_expiry': { 'task': 'base.tasks.notify_task_expiry', - 'schedule': crontab(minute='0', hour='13'), + 'schedule': crontab(minute='5', hour='12'), }, 'notify_top_junior': { 'task': 'base.tasks.notify_top_junior', - 'schedule': crontab(minute='0', hour='*/1'), + 'schedule': crontab(minute='*/5',), }, }