diff --git a/.coverage b/.coverage
new file mode 100644
index 0000000..ad3ab66
Binary files /dev/null and b/.coverage differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7185040
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,23 @@
+/static
+/media
+.idea/
+*.pyc
+media/
+*.name
+*.iml
+*.log
+*.pyo
+.DS_Store
+.idea
+venv/*
+static/*
+*.pem
+*.sqlite3
+/migrations/__pycache__/
+/__pycache__/
+/*.pyc
+*/__pycache__/*.pyc
+__pycache__/
+*.env
+ve/*
+celerybeat-schedule
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..77c6fa8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,11 @@
+FROM python:3.9
+ENV PYTHONUNBUFFERED 1
+RUN mkdir /usr/src/app
+WORKDIR /usr/src/app
+COPY . .
+RUN apt-get update
+RUN apt-get install wkhtmltopdf -y
+RUN apt install -y gdal-bin python3-gdal
+RUN pip install -r requirements.txt
+WORKDIR /usr/src/app
+
diff --git a/account/__init__.py b/account/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/account/admin.py b/account/admin.py
new file mode 100644
index 0000000..be456ab
--- /dev/null
+++ b/account/admin.py
@@ -0,0 +1,62 @@
+"""Account admin"""
+from django.contrib import admin
+
+"""Import django app"""
+from .models import UserEmailOtp, DefaultTaskImages, UserNotification, UserDelete, UserDeviceDetails, ForceUpdate
+# Register your models here.
+
+@admin.register(UserDelete)
+class UserDeleteAdmin(admin.ModelAdmin):
+ """User profile admin"""
+ list_display = ['user', 'old_email', 'd_email']
+
+ def __str__(self):
+ """Return delete user"""
+ return self.user
+@admin.register(UserNotification)
+class UserNotificationAdmin(admin.ModelAdmin):
+ """User profile admin"""
+ list_display = ['user', 'push_notification', 'email_notification', 'sms_notification']
+
+ def __str__(self):
+ """Return image url"""
+ return self.image_url
+@admin.register(DefaultTaskImages)
+class DefaultTaskImagesAdmin(admin.ModelAdmin):
+ """User profile admin"""
+ list_display = ['task_name', 'image_url']
+
+ def __str__(self):
+ """Return image url"""
+ return self.image_url
+
+@admin.register(UserEmailOtp)
+class UserEmailOtpAdmin(admin.ModelAdmin):
+ """User Email otp admin"""
+ list_display = ['email']
+
+ def __str__(self):
+ """Return object in email and otp format"""
+ return self.email + '-' + self.otp
+
+@admin.register(ForceUpdate)
+class ForceUpdateAdmin(admin.ModelAdmin):
+ """Force update"""
+ list_display = ['version', 'device_type']
+ readonly_fields = ('device_type',)
+
+ def has_add_permission(self, request):
+ count = ForceUpdate.objects.all().count()
+ if count < 2:
+ return True
+ return False
+ def has_delete_permission(self, request, obj=None):
+ return False
+@admin.register(UserDeviceDetails)
+class UserDeviceDetailsAdmin(admin.ModelAdmin):
+ """User profile admin"""
+ list_display = ['user', 'device_id']
+
+ def __str__(self):
+ """Return user email"""
+ return self.user.email
diff --git a/account/apps.py b/account/apps.py
new file mode 100644
index 0000000..dc67efe
--- /dev/null
+++ b/account/apps.py
@@ -0,0 +1,9 @@
+"""Account app file"""
+"""Import Django"""
+from django.apps import AppConfig
+
+
+class AccountConfig(AppConfig):
+ """default configurations"""
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'account'
diff --git a/account/custom_middleware.py b/account/custom_middleware.py
new file mode 100644
index 0000000..705cf49
--- /dev/null
+++ b/account/custom_middleware.py
@@ -0,0 +1,73 @@
+"""middleware file"""
+"""Django import"""
+from rest_framework import status
+from rest_framework.response import Response
+from rest_framework.renderers import JSONRenderer
+"""App django"""
+from account.utils import custom_error_response
+from account.models import UserDeviceDetails, ForceUpdate
+from base.messages import ERROR_CODE, SUCCESS_CODE
+from base.constants import NUMBER
+from junior.models import Junior
+from guardian.models import Guardian
+# Custom middleware
+# when user login with
+# multiple device simultaneously
+# It restricted login in
+# multiple devices only
+# user can login in single
+# device at a time"""
+# force update
+# use 308 status code for force update
+
+def custom_response(custom_error, response_status = status.HTTP_404_NOT_FOUND):
+ """custom response"""
+ response = Response(custom_error.data, status=response_status)
+ # Set content type header to "application/json"
+ response['Content-Type'] = 'application/json'
+ # Render the response as JSON
+ renderer = JSONRenderer()
+ response.content = renderer.render(response.data)
+ return response
+class CustomMiddleware(object):
+ """Custom middleware"""
+ def __init__(self, get_response):
+ """response"""
+ self.get_response = get_response
+
+ def __call__(self, request):
+ # Code to be executed before the view is called
+ response = self.get_response(request)
+ # Code to be executed after the view is called
+ device_id = request.META.get('HTTP_DEVICE_ID')
+ user_type = request.META.get('HTTP_USER_TYPE')
+ version = request.META.get('HTTP_VERSION')
+ device_type = str(request.META.get('HTTP_TYPE'))
+
+ api_endpoint = request.path
+ unrestricted_api = ('/api/v1/user/login/', '/api/v1/logout/', '/api/v1/generate-token/')
+ if request.user.is_authenticated:
+ # device details
+ if device_id:
+ device_details = UserDeviceDetails.objects.filter(user=request.user, device_id=device_id).last()
+ if not device_details and api_endpoint not in unrestricted_api:
+ custom_error = custom_error_response(ERROR_CODE['2037'], response_status=status.HTTP_404_NOT_FOUND)
+ response = custom_response(custom_error)
+ if user_type and str(user_type) == str(NUMBER['one']):
+ junior = Junior.objects.filter(auth=request.user, is_active=False).last()
+ if junior:
+ custom_error = custom_error_response(ERROR_CODE['2075'], response_status=status.HTTP_404_NOT_FOUND)
+ response = custom_response(custom_error)
+ elif user_type and str(user_type) == str(NUMBER['two']):
+ guardian = Guardian.objects.filter(user=request.user, is_active=False).last()
+ if guardian:
+ custom_error = custom_error_response(ERROR_CODE['2075'], response_status=status.HTTP_404_NOT_FOUND)
+ response = custom_response(custom_error)
+
+ if version and device_type:
+ force_update = ForceUpdate.objects.filter(version=version, device_type=device_type).last()
+ if not force_update:
+ custom_error = custom_error_response(ERROR_CODE['2079'],
+ response_status=status.HTTP_308_PERMANENT_REDIRECT)
+ response = custom_response(custom_error, status.HTTP_308_PERMANENT_REDIRECT)
+ return response
diff --git a/account/migrations/0001_initial.py b/account/migrations/0001_initial.py
new file mode 100644
index 0000000..3a50122
--- /dev/null
+++ b/account/migrations/0001_initial.py
@@ -0,0 +1,65 @@
+# Generated by Django 4.2.2 on 2023-06-23 12:05
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserEmailOtp',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('email', models.EmailField(max_length=254)),
+ ('otp', models.CharField(max_length=10)),
+ ('is_verified', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('expired_at', models.DateTimeField(blank=True, null=True)),
+ ('is_active', models.BooleanField(default=True)),
+ ],
+ options={
+ 'db_table': 'user_email_otp',
+ },
+ ),
+ migrations.CreateModel(
+ name='UserPhoneOtp',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('country_code', models.IntegerField()),
+ ('phone', models.CharField(max_length=17)),
+ ('otp', models.CharField(max_length=10)),
+ ('is_verified', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('expired_at', models.DateTimeField(blank=True, null=True)),
+ ('is_active', models.BooleanField(default=True)),
+ ],
+ options={
+ 'db_table': 'user_phone_otp',
+ },
+ ),
+ migrations.CreateModel(
+ name='UserProfile',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('user_type', models.CharField(blank=True, choices=[('1', 'junior'), ('2', 'guardian'), ('3', 'superuser')], default=None, max_length=15, null=True)),
+ ('is_verified', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('is_active', models.BooleanField(default=False)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_profile', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'db_table': 'user_profile',
+ },
+ ),
+ ]
diff --git a/account/migrations/0002_useremailotp_user_type.py b/account/migrations/0002_useremailotp_user_type.py
new file mode 100644
index 0000000..9b8821a
--- /dev/null
+++ b/account/migrations/0002_useremailotp_user_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-29 12:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('account', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='useremailotp',
+ name='user_type',
+ field=models.CharField(blank=True, choices=[('1', 'junior'), ('2', 'guardian'), ('3', 'superuser')], default=None, max_length=15, null=True),
+ ),
+ ]
diff --git a/account/migrations/0003_defaulttaskimages.py b/account/migrations/0003_defaulttaskimages.py
new file mode 100644
index 0000000..ec2e030
--- /dev/null
+++ b/account/migrations/0003_defaulttaskimages.py
@@ -0,0 +1,26 @@
+# Generated by Django 4.2.2 on 2023-07-07 10:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('account', '0002_useremailotp_user_type'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='DefaultTaskImages',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('task_name', models.CharField(max_length=15)),
+ ('image_url', models.URLField(blank=True, default=None, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'db_table': 'default_task_image',
+ },
+ ),
+ ]
diff --git a/account/migrations/0004_userdelete.py b/account/migrations/0004_userdelete.py
new file mode 100644
index 0000000..10bd2aa
--- /dev/null
+++ b/account/migrations/0004_userdelete.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.2 on 2023-07-10 09:24
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('account', '0003_defaulttaskimages'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserDelete',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('old_email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Original Email')),
+ ('d_email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Dummy Email')),
+ ('is_active', models.BooleanField(default=True)),
+ ('reason', models.TextField(blank=True, max_length=500, null=True, verbose_name='Reason for Leaving')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='delete_information_set', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'db_table': 'user_delete_information',
+ },
+ ),
+ ]
diff --git a/account/migrations/0005_usernotification.py b/account/migrations/0005_usernotification.py
new file mode 100644
index 0000000..397b633
--- /dev/null
+++ b/account/migrations/0005_usernotification.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.2 on 2023-07-10 12:40
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('account', '0004_userdelete'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserNotification',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('push_notification', models.BooleanField(default=True)),
+ ('email_notification', models.BooleanField(default=True)),
+ ('sms_notification', models.BooleanField(default=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_notification', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'db_table': 'user_notification',
+ },
+ ),
+ ]
diff --git a/account/migrations/0006_alter_useremailotp_options_and_more.py b/account/migrations/0006_alter_useremailotp_options_and_more.py
new file mode 100644
index 0000000..2321b9d
--- /dev/null
+++ b/account/migrations/0006_alter_useremailotp_options_and_more.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.2 on 2023-07-11 11:26
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('account', '0005_usernotification'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='useremailotp',
+ options={'verbose_name': 'User Email OTP', 'verbose_name_plural': 'User Email OTP'},
+ ),
+ migrations.AlterModelOptions(
+ name='usernotification',
+ options={'verbose_name': 'User Notification', 'verbose_name_plural': 'User Notification'},
+ ),
+ ]
diff --git a/account/migrations/0007_alter_defaulttaskimages_options_and_more.py b/account/migrations/0007_alter_defaulttaskimages_options_and_more.py
new file mode 100644
index 0000000..4c134d7
--- /dev/null
+++ b/account/migrations/0007_alter_defaulttaskimages_options_and_more.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.2 on 2023-07-14 09:34
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('account', '0006_alter_useremailotp_options_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='defaulttaskimages',
+ options={'verbose_name': 'Default Task images', 'verbose_name_plural': 'Default Task images'},
+ ),
+ migrations.AlterModelOptions(
+ name='userdelete',
+ options={'verbose_name': 'Deleted User', 'verbose_name_plural': 'Deleted User'},
+ ),
+ ]
diff --git a/account/migrations/0008_userdevicedetails.py b/account/migrations/0008_userdevicedetails.py
new file mode 100644
index 0000000..0b655f6
--- /dev/null
+++ b/account/migrations/0008_userdevicedetails.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.2 on 2023-07-14 11:08
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('account', '0007_alter_defaulttaskimages_options_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserDeviceDetails',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('device_id', models.CharField(max_length=500)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_device_details', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'User Device Details',
+ 'verbose_name_plural': 'User Device Details',
+ 'db_table': 'user_device_details',
+ },
+ ),
+ ]
diff --git a/account/migrations/0009_alter_userdevicedetails_device_id.py b/account/migrations/0009_alter_userdevicedetails_device_id.py
new file mode 100644
index 0000000..2db22a3
--- /dev/null
+++ b/account/migrations/0009_alter_userdevicedetails_device_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-20 11:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('account', '0008_userdevicedetails'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='userdevicedetails',
+ name='device_id',
+ field=models.CharField(blank=True, max_length=500, null=True),
+ ),
+ ]
diff --git a/account/migrations/0010_forceupdate.py b/account/migrations/0010_forceupdate.py
new file mode 100644
index 0000000..84f5b12
--- /dev/null
+++ b/account/migrations/0010_forceupdate.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.2 on 2023-08-22 07:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('account', '0009_alter_userdevicedetails_device_id'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ForceUpdate',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('version', models.CharField(blank=True, max_length=50, null=True)),
+ ('device_type', models.CharField(blank=True, choices=[('1', 'android'), ('2', 'ios')], default=None, max_length=15, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': 'Force Update Version',
+ 'verbose_name_plural': 'Force Update Version',
+ 'db_table': 'force_update',
+ },
+ ),
+ ]
diff --git a/account/migrations/__init__.py b/account/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/account/models.py b/account/models.py
new file mode 100644
index 0000000..d13762b
--- /dev/null
+++ b/account/models.py
@@ -0,0 +1,190 @@
+"""Account module file"""
+"""Django import"""
+from django.db import models
+from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
+"""App import"""
+from base.constants import USER_TYPE, DEVICE_TYPE
+# Create your models here.
+
+class UserProfile(models.Model):
+ """
+ User details
+ """
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_profile')
+ user_type = models.CharField(max_length=15, choices=USER_TYPE, null=True, blank=True, default=None)
+ is_verified = models.BooleanField(default=False)
+
+ # OTP validity
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ is_active = models.BooleanField(default=False)
+
+ class Meta(object):
+ """ Meta information """
+ db_table = 'user_profile'
+
+ def __str__(self):
+ """return phone as an object"""
+ return f'{self.user}'
+
+class UserPhoneOtp(models.Model):
+ """
+ This class is used to verify user email and their contact no.
+ """
+ """user details"""
+ country_code = models.IntegerField()
+ phone = models.CharField(max_length=17)
+ """otp details"""
+ otp = models.CharField(max_length=10)
+ is_verified = models.BooleanField(default=False)
+
+ # OTP validity
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ expired_at = models.DateTimeField(blank=True, null=True)
+ is_active = models.BooleanField(default=True)
+
+ class Meta(object):
+ """ Meta information """
+ db_table = 'user_phone_otp'
+
+ def __str__(self):
+ """return phone as an object"""
+ return self.phone
+
+class UserEmailOtp(models.Model):
+ """
+ This class is used to verify user email and their contact no.
+ """
+ """user details"""
+ email = models.EmailField()
+ """otp details"""
+ otp = models.CharField(max_length=10)
+ is_verified = models.BooleanField(default=False)
+ user_type = models.CharField(max_length=15, choices=USER_TYPE, null=True, blank=True, default=None)
+
+ # OTP validity
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ expired_at = models.DateTimeField(blank=True, null=True)
+ is_active = models.BooleanField(default=True)
+
+ class Meta(object):
+ """ Meta information """
+ db_table = 'user_email_otp'
+ verbose_name = 'User Email OTP'
+ verbose_name_plural = 'User Email OTP'
+
+ def __str__(self):
+ """return phone as an object"""
+ return self.email
+
+class DefaultTaskImages(models.Model):
+ """Default images upload in oss bucket"""
+
+ task_name = models.CharField(max_length=15)
+ image_url = models.URLField(null=True, blank=True, default=None)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(object):
+ """ Meta information """
+ db_table = 'default_task_image'
+ verbose_name = 'Default Task images'
+ verbose_name_plural = 'Default Task images'
+
+ def __str__(self):
+ """return phone as an object"""
+ return self.task_name
+
+class UserDelete(models.Model):
+ """
+ User delete information
+ """
+ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='delete_information_set')
+ """Old email"""
+ old_email = models.EmailField(blank=True, null=True, verbose_name='Original Email')
+ """Dummy email"""
+ d_email = models.EmailField(blank=True, null=True, verbose_name='Dummy Email')
+ is_active = models.BooleanField(default=True)
+ """reason for leaving"""
+ reason = models.TextField(max_length=500, blank=True, null=True, verbose_name='Reason for Leaving')
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta(object):
+ """ Meta information """
+ db_table = 'user_delete_information'
+ verbose_name = 'Deleted User'
+ verbose_name_plural = 'Deleted User'
+
+ def __str__(self):
+ return self.user.email
+
+
+class UserNotification(models.Model):
+ """
+ User notification details
+ """
+ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='user_notification')
+ """Push Notification"""
+ push_notification = models.BooleanField(default=True)
+ """Email Notification"""
+ email_notification = models.BooleanField(default=True)
+ """SMS Notification"""
+ sms_notification = models.BooleanField(default=True)
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(object):
+ """ Meta information """
+ db_table = 'user_notification'
+ verbose_name = 'User Notification'
+ verbose_name_plural = 'User Notification'
+
+ def __str__(self):
+ return self.user.email
+
+
+class UserDeviceDetails(models.Model):
+ """
+ User notification details
+ """
+ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='user_device_details')
+ """Device ID"""
+ device_id = models.CharField(max_length=500, null=True, blank=True)
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(object):
+ """ Meta information """
+ db_table = 'user_device_details'
+ verbose_name = 'User Device Details'
+ verbose_name_plural = 'User Device Details'
+
+ def __str__(self):
+ return self.user.email
+
+
+
+
+class ForceUpdate(models.Model):
+ """
+ Force update
+ """
+ """Version ID"""
+ version = models.CharField(max_length=50, null=True, blank=True)
+ device_type = models.CharField(max_length=15, choices=DEVICE_TYPE, null=True, blank=True, default=None)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(object):
+ """ Meta information """
+ db_table = 'force_update'
+ verbose_name = 'Force Update Version'
+ verbose_name_plural = 'Force Update Version'
+
+ def __str__(self):
+ return self.version
diff --git a/account/serializers.py b/account/serializers.py
new file mode 100644
index 0000000..03160b6
--- /dev/null
+++ b/account/serializers.py
@@ -0,0 +1,409 @@
+"""Account serializer"""
+"""Django Import"""
+# Import Refresh token of jwt
+from rest_framework import serializers
+from django.contrib.auth.models import User
+from rest_framework_simplejwt.tokens import RefreshToken
+import secrets
+"""App import"""
+# Import guardian's model,
+# Import junior's model,
+# Import account's model,
+# Import constant from
+# base package,
+# Import messages from
+# base package,
+# Import some functions
+# from utils file"""
+
+from guardian.models import Guardian
+from junior.models import Junior
+from account.models import UserEmailOtp, DefaultTaskImages, UserDelete, UserNotification, UserPhoneOtp, ForceUpdate
+from base.constants import GUARDIAN, JUNIOR, SUPERUSER, NUMBER
+from base.messages import ERROR_CODE, SUCCESS_CODE, STATUS_CODE_ERROR
+from .utils import delete_user_account_condition_social, delete_user_account_condition
+
+# In this serializer file
+# define google login serializer
+# update junior profile,
+# update guardian profile,
+# super admin serializer,
+# reset password,
+# forgot password,
+# change password,
+# basic junior serializer,
+# basic guardian serializer,
+# user delete account serializer,
+# user notification serializer,
+# update user notification serializer,
+# default task's images serializer,
+# upload default task's images serializer,
+# email verification serializer,
+# phone otp serializer
+
+# create all serializer here
+class GoogleLoginSerializer(serializers.Serializer):
+ """google login serializer"""
+ access_token = serializers.CharField(max_length=5000, required=True)
+
+ class Meta(object):
+ """meta class"""
+ fields = ('access_token',)
+
+
+class UpdateJuniorProfileImageSerializer(serializers.ModelSerializer):
+ """update junior image"""
+ class Meta(object):
+ """Meta info"""
+ model = Junior
+ fields = ['id', 'image']
+
+ def update(self, instance, validated_data):
+ """update image """
+ junior_image = validated_data.get('image', instance.image)
+ instance.image = junior_image
+ instance.save()
+ return instance
+
+
+class UpdateGuardianImageSerializer(serializers.ModelSerializer):
+ """update guardian image"""
+ class Meta(object):
+ """Meta info"""
+ model = Guardian
+ fields = ['id','image']
+
+ def update(self, instance, validated_data):
+ """update image """
+ instance.image = validated_data.get('image', instance.image)
+ instance.save()
+ return instance
+
+
+class ResetPasswordSerializer(serializers.Serializer):
+ """Reset Password after verification"""
+ verification_code = serializers.CharField(max_length=10)
+ password = serializers.CharField(required=True)
+ class Meta(object):
+ """Meta info"""
+ model = User
+
+ def create(self, validated_data):
+ verification_code = validated_data.pop('verification_code')
+ password = validated_data.pop('password')
+ # fetch email otp object of the user
+ user_opt_details = UserEmailOtp.objects.filter(otp=verification_code, is_verified=True).last()
+ if user_opt_details:
+ user_details = User.objects.filter(email=user_opt_details.email).last()
+ if user_details:
+ if user_details.check_password(password):
+ raise serializers.ValidationError({"details":ERROR_CODE['2001'],"code":"400", "status":"failed"})
+ user_details.set_password(password)
+ user_details.save()
+ return {'password':password}
+ return user_opt_details
+ return ''
+
+
+class ChangePasswordSerializer(serializers.Serializer):
+ """Update Password after verification"""
+ current_password = serializers.CharField(max_length=100, required=True)
+ new_password = serializers.CharField(required=True)
+
+ class Meta(object):
+ """Meta info"""
+ model = User
+
+ def validate_current_password(self, value):
+ user = self.context
+ # check old password
+ if self.context.password not in ('', None) and user.check_password(value):
+ return value
+ raise serializers.ValidationError(ERROR_CODE['2015'])
+
+ def create(self, validated_data):
+ """
+ change password
+ """
+ new_password = validated_data.pop('new_password')
+ current_password = validated_data.pop('current_password')
+ # Check new password is different from current password
+ if new_password == current_password:
+ raise serializers.ValidationError({"details": ERROR_CODE['2026']})
+
+ 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(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"""
+ email = serializers.EmailField(required=True)
+ password = serializers.CharField(required=True)
+
+ class Meta:
+ """
+ meta class
+ """
+ model = User
+ fields = ('email', 'password')
+
+ def validate(self, attrs):
+ user = User.objects.filter(email__iexact=attrs['email'], is_superuser=True
+ ).only('id', 'first_name', 'last_name', 'email',
+ 'username', 'is_active', 'is_superuser').first()
+
+ if not user or not user.check_password(attrs['password']):
+ raise serializers.ValidationError({'details': ERROR_CODE['2002']})
+
+ self.context.update({'user': user})
+ return attrs
+
+ def create(self, validated_data):
+ """
+ used to return the user object after validation
+ """
+ return self.context['user']
+
+
+class SuperUserSerializer(serializers.ModelSerializer):
+ """Super admin serializer"""
+ user_type = serializers.SerializerMethodField('get_user_type')
+ auth_token = serializers.SerializerMethodField('get_auth_token')
+ refresh_token = serializers.SerializerMethodField('get_refresh_token')
+
+ def get_auth_token(self, obj):
+ refresh = RefreshToken.for_user(obj)
+ access_token = str(refresh.access_token)
+ return access_token
+
+ def get_refresh_token(self, obj):
+ refresh = RefreshToken.for_user(obj)
+ refresh_token = str(refresh)
+ return refresh_token
+
+ def get_user_type(self, obj):
+ """user type"""
+ return str(NUMBER['three'])
+
+ class Meta(object):
+ """Meta info"""
+ model = User
+ fields = ['id', 'auth_token', 'refresh_token', 'username', 'email', 'first_name',
+ 'last_name', 'is_active', 'user_type']
+
+
+class GuardianSerializer(serializers.ModelSerializer):
+ """guardian serializer"""
+ user_type = serializers.SerializerMethodField('get_user_type')
+ email = serializers.SerializerMethodField('get_auth')
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+ auth_token = serializers.SerializerMethodField('get_auth_token')
+ refresh_token = serializers.SerializerMethodField('get_refresh_token')
+ sign_up = serializers.SerializerMethodField()
+
+ def get_auth_token(self, obj):
+ refresh = RefreshToken.for_user(obj.user)
+ access_token = str(refresh.access_token)
+ return access_token
+
+ def get_refresh_token(self, obj):
+ refresh = RefreshToken.for_user(obj.user)
+ refresh_token = str(refresh)
+ return refresh_token
+
+ def get_user_type(self, obj):
+ """user type"""
+ if self.context.get('user_type', ''):
+ return self.context.get('user_type')
+ # remove the below code once user_type can be passed
+ # from everywhere from where this serializer is being called
+ else:
+ email_verified = UserEmailOtp.objects.filter(
+ email=obj.user.username
+ ).last()
+ if email_verified and email_verified.user_type is not None:
+ return email_verified.user_type
+ return str(NUMBER['two'])
+
+ def get_auth(self, obj):
+ """user email address"""
+ return obj.user.username
+
+ def get_first_name(self, obj):
+ """user first name"""
+ return obj.user.first_name
+
+ def get_last_name(self, obj):
+ """user last name"""
+ return obj.user.last_name
+
+ def get_sign_up(self, obj):
+ return True if self.context.get('sign_up', '') else False
+
+ class Meta(object):
+ """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', 'is_deleted',
+ 'is_complete_profile', 'passcode', 'image', 'created_at', 'updated_at', 'user_type',
+ 'country_name', 'sign_up']
+
+
+class JuniorSerializer(serializers.ModelSerializer):
+ """junior serializer"""
+ user_type = serializers.SerializerMethodField('get_user_type')
+ email = serializers.SerializerMethodField('get_auth')
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+ auth_token = serializers.SerializerMethodField('get_auth_token')
+ refresh_token = serializers.SerializerMethodField('get_refresh_token')
+ sign_up = serializers.SerializerMethodField()
+
+ def get_auth_token(self, obj):
+ refresh = RefreshToken.for_user(obj.auth)
+ access_token = str(refresh.access_token)
+ return access_token
+
+ def get_refresh_token(self, obj):
+ refresh = RefreshToken.for_user(obj.auth)
+ refresh_token = str(refresh)
+ return refresh_token
+
+ def get_user_type(self, obj):
+ email_verified = UserEmailOtp.objects.filter(email=obj.auth.username).last()
+ if email_verified and email_verified.user_type is not None:
+ return email_verified.user_type
+ return str(NUMBER['one'])
+
+ def get_auth(self, obj):
+ return obj.auth.username
+
+ def get_first_name(self, obj):
+ return obj.auth.first_name
+
+ def get_last_name(self, obj):
+ return obj.auth.last_name
+
+ def get_sign_up(self, obj):
+ return True if self.context.get('sign_up', '') else False
+
+ class Meta(object):
+ """Meta info"""
+ 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_deleted', 'sign_up']
+
+class EmailVerificationSerializer(serializers.ModelSerializer):
+ """Email verification serializer"""
+ class Meta(object):
+ """Meta info"""
+ model = UserEmailOtp
+ fields = ('email',)
+
+
+
+class DefaultTaskImagesSerializer(serializers.ModelSerializer):
+ """Update Password after verification"""
+ class Meta(object):
+ """Meta info"""
+ model = DefaultTaskImages
+ fields = ['id', 'task_name', 'image_url']
+ def create(self, validated_data):
+ # create default task object
+ data = DefaultTaskImages.objects.create(**validated_data)
+ return data
+
+class DefaultTaskImagesDetailsSerializer(serializers.ModelSerializer):
+ """Update Password after verification"""
+ class Meta(object):
+ """Meta info"""
+ model = DefaultTaskImages
+ fields = '__all__'
+
+class UserDeleteSerializer(serializers.ModelSerializer):
+ """User Delete Serializer"""
+ class Meta(object):
+ """Meta Information"""
+ model = UserDelete
+ fields = ['id','reason']
+ def create(self, validated_data):
+ user = self.context['user']
+ user_type = str(self.context['user_type'])
+ data = validated_data.get('reason')
+ passwd = self.context['password']
+ signup_method = self.context['signup_method']
+ random_num = secrets.randbelow(10001)
+ user_tb = User.objects.filter(id=user.id).last()
+ user_type_datas = UserEmailOtp.objects.filter(email=user.email).last()
+ # check password and sign up method
+ if user_tb and user_tb.check_password(passwd) and signup_method == str(NUMBER['one']):
+ user_type_data = user_type_datas.user_type
+ instance = delete_user_account_condition(user, user_type_data, user_type, user_tb, data, random_num)
+ return instance
+ elif user_tb and passwd is None and signup_method in ['2','3']:
+ inst = delete_user_account_condition_social(user, user_type, user_tb, data, random_num)
+ return inst
+ else:
+ raise serializers.ValidationError({"details": ERROR_CODE['2031'], "code": "400", "status": "failed"})
+
+
+class UserNotificationSerializer(serializers.ModelSerializer):
+ """User Notification serializer"""
+ class Meta(object):
+ """Meta info"""
+ model = UserNotification
+ fields = '__all__'
+
+
+class UpdateUserNotificationSerializer(serializers.ModelSerializer):
+ """Update User Notification serializer"""
+ class Meta(object):
+ """Meta info"""
+ model = UserNotification
+ fields = ['push_notification', 'email_notification', 'sms_notification']
+
+ def create(self, validated_data):
+ instance, _ = UserNotification.objects.update_or_create(
+ user=self.context,
+ defaults={
+ 'push_notification': validated_data.get('push_notification'),
+ 'email_notification': validated_data.get('email_notification'),
+ 'sms_notification': validated_data.get('sms_notification', False),
+ })
+ return instance
+
+
+class UserPhoneOtpSerializer(serializers.ModelSerializer):
+ """User Phone serializers"""
+ class Meta(object):
+ """Meta info"""
+ model = UserPhoneOtp
+ fields = '__all__'
+
+class ForceUpdateSerializer(serializers.ModelSerializer):
+ """ ForceUpdate Serializer
+ """
+
+ class Meta(object):
+ """ meta info """
+ model = ForceUpdate
+ fields = ('id', 'version', 'device_type')
diff --git a/account/templates/templated_email/email_base.email b/account/templates/templated_email/email_base.email
new file mode 100644
index 0000000..5721e28
--- /dev/null
+++ b/account/templates/templated_email/email_base.email
@@ -0,0 +1,54 @@
+
+{% block subject %}DinDin{% endblock %}
+{% load static %}
+
+{% block html %}
+
+
+
+
+ Zod Bank | OTP
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block plain %}
+ {% endblock %}
+
+
+ -
+ Cheers!
+ Zod Bank Team
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/account/templates/templated_email/email_otp_verification.email b/account/templates/templated_email/email_otp_verification.email
new file mode 100644
index 0000000..8b3c693
--- /dev/null
+++ b/account/templates/templated_email/email_otp_verification.email
@@ -0,0 +1,23 @@
+{% extends "templated_email/email_base.email" %}
+
+{% block subject %}
+ OTP Verification
+{% endblock %}
+
+{% block plain %}
+
+
+
+ Hi User,
+
+
+
+
+
+
+ You are receiving this email for email verification. Please use {{ otp }} as the verification code for your email address & username.
+
+
+
+
+{% endblock %}
diff --git a/account/templates/templated_email/email_reset_verification.email b/account/templates/templated_email/email_reset_verification.email
new file mode 100644
index 0000000..e2f8ebf
--- /dev/null
+++ b/account/templates/templated_email/email_reset_verification.email
@@ -0,0 +1,23 @@
+{% extends "templated_email/email_base.email" %}
+
+{% block subject %}
+ Password Reset Verification Code
+{% endblock %}
+
+{% block plain %}
+
+
+
+ Hi User,
+
+
+
+
+
+
+ You are receiving this email for reset password verification. Please use {{ verification_code }} as the verification code.
+
+
+
+
+{% endblock %}
diff --git a/account/templates/templated_email/junior_approval_mail.email b/account/templates/templated_email/junior_approval_mail.email
new file mode 100644
index 0000000..e9884d6
--- /dev/null
+++ b/account/templates/templated_email/junior_approval_mail.email
@@ -0,0 +1,23 @@
+{% extends "templated_email/email_base.email" %}
+
+{% block subject %}
+ Approval Email
+{% endblock %}
+
+{% block plain %}
+
+
+
+ Hello,
+
+
+
+
+
+
+ Please approve {{full_name}} for accessing the Zod bank platform as a junior.
+
+
+
+
+{% endblock %}
diff --git a/account/templates/templated_email/junior_notification_email.email b/account/templates/templated_email/junior_notification_email.email
new file mode 100644
index 0000000..9f489ce
--- /dev/null
+++ b/account/templates/templated_email/junior_notification_email.email
@@ -0,0 +1,24 @@
+{% extends "templated_email/email_base.email" %}
+
+{% block subject %}
+ Invitation Email
+{% endblock %}
+
+{% block plain %}
+
+
+
+ Hi {{full_name}},
+
+
+
+
+
+
+ You are receiving this email for joining the ZOD bank platform. Please use {{ url }} link to join the platform.
+ Your credentials are:- username = {{email}} and password {{password}} Below are the steps to complete the account and how to use this platform.
+
+
+
+
+{% endblock %}
diff --git a/account/templates/templated_email/support_mail.email b/account/templates/templated_email/support_mail.email
new file mode 100644
index 0000000..20fcdb1
--- /dev/null
+++ b/account/templates/templated_email/support_mail.email
@@ -0,0 +1,22 @@
+{% extends "templated_email/email_base.email" %}
+
+{% block subject %}
+ Support Mail
+{% endblock %}
+
+{% block plain %}
+
+
+
+ Hi Support Team,
+
+
+
+
+
+
+ {{name}} have some queries and need some support. Please support them by using their email address {{sender}} . Queries are:-
{{ message }}
+
+
+
+{% endblock %}
diff --git a/account/templates/templated_email/user_activate.email b/account/templates/templated_email/user_activate.email
new file mode 100644
index 0000000..24ba5f8
--- /dev/null
+++ b/account/templates/templated_email/user_activate.email
@@ -0,0 +1,22 @@
+{% extends "templated_email/email_base.email" %}
+
+{% block subject %}
+ Account Activated
+{% endblock %}
+
+{% block plain %}
+
+
+
+ Hi User,
+
+
+
+
+
+
+ We're pleased to inform you that your account has been successfully reactivated by our admin team. Welcome back to ZOD ! You can now access all the features and services as before. If you have any questions or need assistance, please feel free to reach out to our support team. Thank you for being a valued member of our community.
+
+
+
+{% endblock %}
diff --git a/account/templates/templated_email/user_deactivate.email b/account/templates/templated_email/user_deactivate.email
new file mode 100644
index 0000000..90b3ee1
--- /dev/null
+++ b/account/templates/templated_email/user_deactivate.email
@@ -0,0 +1,22 @@
+{% extends "templated_email/email_base.email" %}
+
+{% block subject %}
+ Account Deactivated
+{% endblock %}
+
+{% block plain %}
+
+
+
+ Hi User,
+
+
+
+
+
+
+ Your account has been deactivated by admin. Please reach out to the admin for assistance.
+
+
+
+{% endblock %}
diff --git a/account/tests.py b/account/tests.py
new file mode 100644
index 0000000..9b8af76
--- /dev/null
+++ b/account/tests.py
@@ -0,0 +1,60 @@
+"""
+test cases file of account
+"""
+# django imports
+from django.test import TestCase
+from rest_framework.test import APIClient
+from rest_framework import status
+from django.contrib.auth.models import User
+from django.urls import reverse
+from rest_framework_simplejwt.tokens import RefreshToken
+
+
+class UserLoginTestCase(TestCase):
+ """
+ test cases for login
+ """
+ def setUp(self):
+ """
+ set up data
+ :return:
+ """
+ self.client = APIClient()
+ self.user_email = 'user@example.com'
+ self.user = User.objects.create_superuser(username=self.user_email, email=self.user_email)
+ self.user.set_password('user@1234')
+ self.user.save()
+
+ def test_admin_login_success(self):
+ """
+ test admin login with valid credentials
+ :return:
+ """
+ url = reverse('account:admin-login')
+ data = {
+ 'email': self.user_email,
+ 'password': 'user@1234',
+ }
+ response = self.client.post(url, data, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('auth_token', response.data['data'])
+ self.assertIn('refresh_token', response.data['data'])
+ self.assertEqual(response.data['data']['username'], data['email'])
+
+ def test_admin_login_invalid_credentials(self):
+ """
+ test admin login with invalid credentials
+ :return:
+ """
+ url = reverse('account:admin-login')
+ data = {
+ 'email': self.user_email,
+ 'password': 'user@1235',
+ }
+ response = self.client.post(url, data, format='json')
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertNotIn('auth_token', response.data)
+ self.assertNotIn('refresh_token', response.data)
+
+ # Add more test cases as needed
+
diff --git a/account/urls.py b/account/urls.py
new file mode 100644
index 0000000..4944d67
--- /dev/null
+++ b/account/urls.py
@@ -0,0 +1,71 @@
+""" Urls files"""
+"""Django import"""
+from django.urls import path, include
+"""Third party import"""
+from rest_framework import routers
+# Import view functions
+# UserLogin views,
+# SendPhoneOtp views,
+# UserPhoneVerification views,
+# UserEmailVerification views,
+# ReSendEmailOtp views,
+# ForgotPasswordAPIView views,
+# ResetPasswordAPIView views,
+# ChangePasswordAPIView views,
+# UpdateProfileImage views,
+# GoogleLoginViewSet views,
+# SigninWithApple views,
+# ProfileAPIViewSet views,
+# UploadImageAPIViewSet views,
+# DefaultImageAPIViewSet views,
+# DeleteUserProfileAPIViewSet views,
+# UserNotificationAPIViewSet views,
+# UpdateUserNotificationAPIViewSet views,
+# SendSupportEmail views,
+# LogoutAPIView views,
+# AccessTokenAPIView views"""
+from .views import (UserLogin, SendPhoneOtp, UserPhoneVerification, UserEmailVerification, ReSendEmailOtp,
+ ForgotPasswordAPIView, ResetPasswordAPIView, ChangePasswordAPIView, UpdateProfileImage,
+ GoogleLoginViewSet, SigninWithApple, ProfileAPIViewSet, UploadImageAPIViewSet,
+ DefaultImageAPIViewSet, DeleteUserProfileAPIViewSet, UserNotificationAPIViewSet,
+ UpdateUserNotificationAPIViewSet, SendSupportEmail, LogoutAPIView, AccessTokenAPIView,
+ AdminLoginViewSet, ForceUpdateViewSet)
+"""Router"""
+router = routers.SimpleRouter()
+
+"""API End points with router"""
+router.register('user', UserLogin, basename='user')
+"""super admin login"""
+router.register('admin', AdminLoginViewSet, basename='admin')
+"""google login end point"""
+router.register('google-login', GoogleLoginViewSet, basename='admin')
+"""email verification end point"""
+router.register('user-email-verification', UserEmailVerification, basename='user-email-verification')
+"""Resend email otp end point"""
+router.register('resend-email-otp', ReSendEmailOtp, basename='resend-email-otp')
+"""Profile end point"""
+router.register('profile', ProfileAPIViewSet, basename='profile')
+"""Upload default task image end point"""
+router.register('upload-default-task-image', UploadImageAPIViewSet, basename='upload-default-task-image')
+"""Fetch default task image end point"""
+router.register('default-task-image', DefaultImageAPIViewSet, basename='default-task-image')
+"""Delete user account"""
+router.register('delete', DeleteUserProfileAPIViewSet, basename='delete')
+"""user account notification"""
+router.register('user-notification', UserNotificationAPIViewSet, basename='user-notification')
+"""update user account notification"""
+router.register('update-user-notification', UpdateUserNotificationAPIViewSet, basename='update-user-notification')
+# Force update entry API
+router.register('force-update', ForceUpdateViewSet, basename='force-update')
+"""Define url pattern"""
+urlpatterns = [
+ path('api/v1/', include(router.urls)),
+ path('api/v1/forgot-password/', ForgotPasswordAPIView.as_view()),
+ path('api/v1/reset-password/', ResetPasswordAPIView.as_view()),
+ path('api/v1/change-password/', ChangePasswordAPIView.as_view()),
+ path('api/v1/update-profile-image/', UpdateProfileImage.as_view()),
+ path('api/v1/apple-login/', SigninWithApple.as_view(), name='signup_with_apple'),
+ path('api/v1/send-support-email/', SendSupportEmail.as_view(), name='send-support-email'),
+ path('api/v1/logout/', LogoutAPIView.as_view(), name='logout'),
+ path('api/v1/generate-token/', AccessTokenAPIView.as_view(), name='generate-token')
+]
diff --git a/account/utils.py b/account/utils.py
new file mode 100644
index 0000000..564472b
--- /dev/null
+++ b/account/utils.py
@@ -0,0 +1,326 @@
+"""Account utils"""
+from celery import shared_task
+import random
+"""Import django"""
+from django.conf import settings
+from rest_framework import viewsets, status
+from rest_framework.response import Response
+"""Third party Django app"""
+from templated_email import send_templated_mail
+import jwt
+import string
+from datetime import datetime
+from calendar import timegm
+from uuid import uuid4
+# Import secrets module for generating random number
+import secrets
+from rest_framework import serializers
+# Django App Import
+# Import models from junior App,
+# Import models from guardian App,
+# Import models from account App,
+# Import messages from base package"""
+from junior.models import Junior
+from guardian.models import Guardian
+from account.models import UserDelete, UserDeviceDetails
+from base.messages import ERROR_CODE
+from django.utils import timezone
+from base.constants import NUMBER
+from junior.models import JuniorPoints
+# Define delete
+# user account condition,
+# Define delete
+# user account
+# condition for social
+# login account,
+# Update junior account,
+# Update guardian account,
+# Define custom email for otp verification,
+# Define support email for user's query,
+# Define custom success response,
+# Define custom error response,
+# Generate access token,
+# refresh token by using jwt,
+# Define function for generating
+# guardian code, junior code,
+# referral code,
+# Define function for generating
+# alphanumeric code
+# otp expiry
+def delete_user_account_condition(user, user_type_data, user_type, user_tb, data, random_num):
+ """delete user account"""
+ if user_type == '1' and user_type_data == '1':
+ junior_account_update(user_tb)
+ elif user_type == '2' and user_type_data == '2':
+ guardian_account_update(user_tb)
+ else:
+ raise serializers.ValidationError({"details": ERROR_CODE['2030'], "code": "400", "status": "failed"})
+ user_tb.email = str(random_num) + str('@D_') + '{}'.format(user_tb.username).lower()
+ user_tb.username = str(random_num) + str('@D_') + '{}'.format(user_tb.username).lower()
+ d_email = user_tb.email
+ o_mail = user.email
+ # update user email with dummy email
+ user_tb.save()
+ """create object in user delete model"""
+ instance = UserDelete.objects.create(user=user_tb, d_email=d_email, old_email=o_mail,
+ is_active=True, reason=data)
+
+ return instance
+
+def delete_user_account_condition_social(user, user_type,user_tb, data, random_num):
+ """delete user account"""
+ if user_type == '1':
+ junior_account_update(user_tb)
+ elif user_type == '2':
+ guardian_account_update(user_tb)
+ else:
+ raise serializers.ValidationError({"details": ERROR_CODE['2030'], "code": "400", "status": "failed"})
+ user_tb.email = str(random_num) + str('@D_') + '{}'.format(user_tb.username).lower()
+ user_tb.username = str(random_num) + str('@D_') + '{}'.format(user_tb.username).lower()
+ dummy_email = user_tb.email
+ old_mail = user.email
+ # update user email with dummy email
+ user_tb.save()
+ """create object in user delete model"""
+ instance_data = UserDelete.objects.create(user=user_tb, d_email=dummy_email, old_email=old_mail,
+ is_active=True, reason=data)
+
+ return instance_data
+def junior_account_update(user_tb):
+ """junior account delete"""
+ junior_data = Junior.objects.filter(auth__email=user_tb.email).first()
+ if junior_data:
+ # Update junior account
+ junior_data.is_active = False
+ junior_data.is_verified = False
+ junior_data.guardian_code = None
+ junior_data.guardian_code_status = None
+ junior_data.is_deleted = True
+ junior_data.save()
+ JuniorPoints.objects.filter(junior=junior_data).delete()
+
+def guardian_account_update(user_tb):
+ """update guardian account after delete the user account"""
+ guardian_data = Guardian.objects.filter(user__email=user_tb.email).first()
+ if guardian_data:
+ # 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"""
+ for data in jun_data:
+ data.guardian_code.remove(guardian_data.guardian_code)
+ data.save()
+@shared_task()
+def send_otp_email(recipient_email, otp):
+ """Send otp on email with template"""
+ from_email = settings.EMAIL_FROM_ADDRESS
+ recipient_list = [recipient_email]
+ """Send otp on email"""
+ send_templated_mail(
+ template_name='email_otp_verification.email',
+ from_email=from_email,
+ recipient_list=recipient_list,
+ context={
+ 'otp': 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):
+ """
+ Used to store the device id of the user
+ user: user object
+ device_id: string
+ return
+ """
+ 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, message):
+ """Send otp on email with template"""
+ to_email = [settings.EMAIL_FROM_ADDRESS]
+ from_email = settings.DEFAULT_ADDRESS
+ """Send support email to zod bank support team"""
+ send_templated_mail(
+ template_name='support_mail.email',
+ from_email=from_email,
+ recipient_list=to_email,
+ context={
+ 'name': name.title(),
+ 'sender': sender,
+ 'message': message
+ }
+ )
+ return name
+
+
+def custom_response(detail, data=None, response_status=status.HTTP_200_OK, count=None):
+ """Custom response code"""
+ if not data:
+ """when data is none"""
+ data = None
+ return Response({"data": data, "message": detail, "status": "success",
+ "code": response_status, "count": count})
+
+
+def custom_error_response(detail, response_status):
+ """
+ function is used for getting same global error response for all
+ :param detail: error message .
+ :param response_status: http status.
+ :return: Json response
+ """
+ if not detail:
+ """when details is empty"""
+ detail = {}
+ if response_status == 406:
+ return Response({"error": detail, "status": "failed", "code": response_status,},
+ status=status.HTTP_308_PERMANENT_REDIRECT)
+ else:
+ return Response({"error": detail, "status": "failed", "code": response_status},
+ status=status.HTTP_400_BAD_REQUEST)
+
+def get_user_data(attrs):
+ """
+ used to decode token
+ """
+ user_data = jwt.decode(jwt=attrs['token'], options={'verify_signature': False},
+ algorithms=['RS256'])
+ return user_data
+
+
+def generate_jwt_token(token_type: str, now_time: int, data: dict = dict):
+ """
+ used to generate jwt token
+ """
+ if type(data) == type:
+ data = {}
+ """Update data dictionary"""
+ data.update({
+ 'token_type': token_type,
+ 'iss': 'your_site_url',
+ 'iat': timegm(datetime.utcnow().utctimetuple()),
+ 'jti': uuid4().hex
+ })
+ """Access and Refresh token"""
+ TOKEN_TYPE = ["access", "refresh"]
+ if token_type == TOKEN_TYPE[1]:
+ """Refresh token"""
+ exp = now_time + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME']
+ else:
+ """access token"""
+ exp = now_time + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME']
+
+ data.update({
+ "exp": timegm(exp.utctimetuple())
+ })
+
+
+ signing_key = secrets.token_hex(32)
+
+ return jwt.encode(payload=data, key=signing_key,
+ algorithm='HS256')
+
+
+def get_token(data: dict = dict):
+ """ create access and refresh token """
+ now_time = datetime.utcnow()
+ """generate access token"""
+ access = generate_jwt_token('access', now_time, data)
+ """generate refresh token"""
+ refresh = generate_jwt_token('refresh', now_time, data)
+
+ return {
+ 'access': access,
+ 'refresh': refresh
+ }
+
+
+def generate_alphanumeric_code(length):
+ """Generate alphanumeric code"""
+ alphabet = string.ascii_letters + string.digits
+ code = ''.join(secrets.choice(alphabet) for _ in range(length))
+ return code
+
+
+def generate_code(value, user_id):
+ """generate referral, junior and guardian code"""
+ code = value + str(user_id).zfill(3)
+ return code
+
+
+OTP_EXPIRY = timezone.now() + timezone.timedelta(days=1)
+
+
+def get_user_full_name(user_obj):
+ """
+ to get user's full name
+ """
+ return f"{user_obj.first_name} {user_obj.last_name}" if user_obj.first_name or user_obj.last_name else "User"
+
+
+def make_special_password(length=10):
+ """
+ to make secured password
+ :param length:
+ :return:
+ """
+ # Define character sets
+ lowercase_letters = string.ascii_lowercase
+ uppercase_letters = string.ascii_uppercase
+ digits = string.digits
+ special_characters = '@#$%&*?'
+
+ # Combine character sets
+ alphabets = lowercase_letters + uppercase_letters
+
+ # Create a password with random characters
+ password = [
+ secrets.choice(uppercase_letters) +
+ secrets.choice(lowercase_letters) +
+ secrets.choice(digits) +
+ secrets.choice(special_characters) +
+ ''.join(secrets.choice(alphabets) for _ in range(length - 4))
+ ]
+
+ return ''.join(password)
+
+def task_status_fun(status_value):
+ """task status"""
+ task_status_value = ['1']
+ if str(status_value) == '2':
+ task_status_value = ['2', '4']
+ elif str(status_value) == '3':
+ task_status_value = ['3', '5', '6']
+ return task_status_value
diff --git a/account/views.py b/account/views.py
new file mode 100644
index 0000000..a656e0b
--- /dev/null
+++ b/account/views.py
@@ -0,0 +1,770 @@
+"""Account view """
+import threading
+
+from notifications.utils import remove_fcm_token
+
+# django imports
+from rest_framework.viewsets import GenericViewSet, mixins
+from datetime import datetime, timedelta
+from rest_framework import viewsets, status, views
+from rest_framework.decorators import action
+import logging
+from django.utils import timezone
+import jwt
+from django.contrib.auth import logout
+from django.contrib.auth import authenticate, login
+from rest_framework.permissions import IsAuthenticated
+from templated_email import send_templated_mail
+import google.oauth2.credentials
+import google.auth.transport.requests
+from rest_framework import status
+import requests
+from rest_framework.response import Response
+from rest_framework import mixins
+from django.conf import settings
+
+# local imports
+from guardian.models import Guardian
+from junior.models import Junior, JuniorPoints
+from guardian.utils import upload_image_to_alibaba
+from account.models import (UserDeviceDetails, UserPhoneOtp, UserEmailOtp, DefaultTaskImages, UserNotification,
+ ForceUpdate)
+from django.contrib.auth.models import User
+from .serializers import (SuperUserSerializer, GuardianSerializer, JuniorSerializer, EmailVerificationSerializer,
+ ForgotPasswordSerializer, ResetPasswordSerializer, ChangePasswordSerializer,
+ GoogleLoginSerializer, UpdateGuardianImageSerializer, UpdateJuniorProfileImageSerializer,
+ DefaultTaskImagesSerializer, DefaultTaskImagesDetailsSerializer, UserDeleteSerializer,
+ UserNotificationSerializer, UpdateUserNotificationSerializer, UserPhoneOtpSerializer,
+ AdminLoginSerializer, ForceUpdateSerializer)
+from rest_framework_simplejwt.tokens import RefreshToken
+from base.messages import ERROR_CODE, SUCCESS_CODE
+from base.constants import NUMBER, ZOD, JUN, GRD, USER_TYPE_FLAG
+from guardian.tasks import generate_otp
+from account.utils import (send_otp_email, send_support_email, custom_response, custom_error_response,
+ generate_code, OTP_EXPIRY, user_device_details, send_all_email)
+from junior.serializers import JuniorProfileSerializer
+from guardian.serializers import GuardianProfileSerializer
+
+class GoogleLoginMixin(object):
+ """google login mixin"""
+ @staticmethod
+ def google_login(request):
+ """google login function"""
+ access_token = request.data.get('access_token')
+ user_type = request.META.get('HTTP_USER_TYPE')
+ device_id = request.META.get('HTTP_DEVICE_ID')
+ if not access_token:
+ return Response({'error': 'Access token is required.'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ # Validate the access token and obtain the user's email and name
+ credentials = google.oauth2.credentials.Credentials.from_authorized_user_info(
+ info={
+ 'access_token': access_token,
+ 'token_uri': 'https://oauth2.googleapis.com/token',
+ 'client_id': settings.GOOGLE_CLIENT_ID,
+ 'client_secret': settings.GOOGLE_CLIENT_SECRET,
+ 'refresh_token': None,
+ }
+ )
+ user_info_endpoint = f'https://www.googleapis.com/oauth2/v3/userinfo?access_token={access_token}'
+ headers = {'Authorization': f'Bearer {credentials.token}'}
+ response = requests.get(user_info_endpoint, headers=headers)
+ response.raise_for_status()
+ user_info = response.json()
+ email = user_info['email']
+ first_name = user_info['given_name']
+ last_name = user_info['family_name'] if 'family_name' in user_info and user_info['family_name'] else user_info['given_name']
+ profile_picture = user_info['picture']
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+ # Check if the user exists in your database or create a new user
+ # ...
+ user_data = User.objects.filter(email__iexact=email)
+ if user_data.exists():
+ if str(user_type) == '1':
+ junior_query = Junior.objects.filter(auth=user_data.last()).last()
+ if not junior_query:
+ return custom_error_response(
+ ERROR_CODE["2071"],
+ response_status=status.HTTP_400_BAD_REQUEST
+ )
+ if not junior_query.is_active:
+ return custom_error_response(
+ ERROR_CODE["2075"],
+ response_status=status.HTTP_404_NOT_FOUND
+ )
+ serializer = JuniorSerializer(junior_query)
+ elif str(user_type) == '2':
+ guardian_query = Guardian.objects.filter(user=user_data.last()).last()
+ if not guardian_query:
+ return custom_error_response(
+ ERROR_CODE["2070"],
+ response_status=status.HTTP_400_BAD_REQUEST
+ )
+ if not guardian_query.is_active:
+ return custom_error_response(
+ ERROR_CODE["2075"],
+ response_status=status.HTTP_404_NOT_FOUND
+ )
+ serializer = GuardianSerializer(guardian_query)
+ else:
+ return custom_error_response(
+ ERROR_CODE["2069"],
+ response_status=status.HTTP_400_BAD_REQUEST
+ )
+ device_detail, created = UserDeviceDetails.objects.get_or_create(user=user_data.last())
+ if device_detail:
+ device_detail.device_id = device_id
+ device_detail.save()
+ return custom_response(SUCCESS_CODE['3003'], serializer.data,
+ response_status=status.HTTP_200_OK)
+
+ else:
+ user_obj = User.objects.create(username=email, email=email, first_name=first_name, last_name=last_name)
+ if str(user_type) == '1':
+ junior_query = Junior.objects.create(auth=user_obj, is_verified=True, is_active=True,
+ image=profile_picture, signup_method='2',
+ junior_code=generate_code(JUN, user_obj.id),
+ referral_code=generate_code(ZOD, user_obj.id)
+ )
+ serializer = JuniorSerializer(junior_query, context={'sign_up': True})
+ position = Junior.objects.all().count()
+ JuniorPoints.objects.create(junior=junior_query, position=position)
+ elif str(user_type) == '2':
+ guardian_query = Guardian.objects.create(user=user_obj, is_verified=True, is_active=True,
+ image=profile_picture,signup_method='2',
+ guardian_code=generate_code(GRD, user_obj.id),
+ referral_code=generate_code(ZOD, user_obj.id)
+ )
+ serializer = GuardianSerializer(guardian_query, context={'sign_up': True})
+ else:
+ user_obj.delete()
+ return custom_error_response(
+ ERROR_CODE["2069"],
+ response_status=status.HTTP_400_BAD_REQUEST
+ )
+ device_detail, created = UserDeviceDetails.objects.get_or_create(user=user_obj)
+ if device_detail:
+ device_detail.device_id = device_id
+ device_detail.save()
+ # Return a JSON response with the user's email and name
+ return custom_response(SUCCESS_CODE['3003'], serializer.data,
+ response_status=status.HTTP_200_OK)
+
+
+class GoogleLoginViewSet(GoogleLoginMixin, viewsets.GenericViewSet):
+ """Google login viewset"""
+ serializer_class = GoogleLoginSerializer
+
+ def create(self, request):
+ """Payload
+ {
+ "access_token",
+ "user_type": "1"
+ }"""
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ return self.google_login(request)
+
+class SigninWithApple(views.APIView):
+ """This API is for sign in with Apple for app.
+ Payload
+ {
+ "access_token",
+ "user_type": "1"
+ }"""
+ def post(self, request):
+ token = request.data.get("access_token")
+ user_type = request.META.get('HTTP_USER_TYPE')
+ device_id = request.META.get('HTTP_DEVICE_ID')
+ try:
+ decoded_data = jwt.decode(token, options={"verify_signature": False})
+ user_data = {"email": decoded_data.get('email'), "username": decoded_data.get('email'), "is_active": True}
+ if decoded_data.get("email"):
+ try:
+ user = User.objects.get(email__iexact=decoded_data.get("email"))
+ if str(user_type) == '1':
+ junior_data = Junior.objects.filter(auth=user).last()
+ if not junior_data:
+ return custom_error_response(
+ ERROR_CODE["2071"],
+ response_status=status.HTTP_400_BAD_REQUEST
+ )
+ serializer = JuniorSerializer(junior_data)
+ elif str(user_type) == '2':
+ guardian_data = Guardian.objects.filter(user=user).last()
+ if not guardian_data:
+ return custom_error_response(
+ ERROR_CODE["2070"],
+ response_status=status.HTTP_400_BAD_REQUEST
+ )
+ serializer = GuardianSerializer(guardian_data)
+ else:
+ return custom_error_response(
+ ERROR_CODE["2069"],
+ response_status=status.HTTP_400_BAD_REQUEST
+ )
+ device_detail, created = UserDeviceDetails.objects.get_or_create(user=user)
+ if device_detail:
+ device_detail.device_id = device_id
+ device_detail.save()
+ return custom_response(SUCCESS_CODE['3003'], serializer.data,
+ response_status=status.HTTP_200_OK)
+
+ except User.DoesNotExist:
+ user = User.objects.create(**user_data)
+ if str(user_type) == '1':
+ junior_query = Junior.objects.create(auth=user, is_verified=True, is_active=True,
+ signup_method='3',
+ junior_code=generate_code(JUN, user.id),
+ referral_code=generate_code(ZOD, user.id))
+ serializer = JuniorSerializer(junior_query, context={'sign_up': True})
+ position = Junior.objects.all().count()
+ JuniorPoints.objects.create(junior=junior_query, position=position)
+ elif str(user_type) == '2':
+ guardian_query = Guardian.objects.create(user=user, is_verified=True, is_active=True,
+ signup_method='3',
+ guardian_code=generate_code(GRD, user.id),
+ referral_code=generate_code(ZOD, user.id))
+ serializer = GuardianSerializer(guardian_query, context={'sign_up': True})
+ else:
+ user.delete()
+ return custom_error_response(
+ ERROR_CODE["2069"],
+ response_status=status.HTTP_400_BAD_REQUEST
+ )
+ device_detail, created = UserDeviceDetails.objects.get_or_create(user=user)
+ if device_detail:
+ device_detail.device_id = device_id
+ device_detail.save()
+ return custom_response(SUCCESS_CODE['3003'], serializer.data,
+ response_status=status.HTTP_200_OK)
+ except Exception as e:
+ logging.error(e)
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class UpdateProfileImage(views.APIView):
+ """Update profile image"""
+ permission_classes = [IsAuthenticated]
+ def put(self, request, format=None):
+ try:
+ image = request.data['image']
+ if image and image.size == NUMBER['zero']:
+ return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST)
+ filename = f"images/{image.name}"
+ image_url = upload_image_to_alibaba(image, filename)
+ image_data = image_url
+ if str(request.data['user_type']) == '1':
+ junior_query = Junior.objects.filter(auth=request.user).last()
+ serializer = UpdateJuniorProfileImageSerializer(junior_query,
+ data={'image':image_data}, partial=True)
+ elif str(request.data['user_type']) == '2':
+ guardian_query = Guardian.objects.filter(user=request.user).last()
+ serializer = UpdateGuardianImageSerializer(guardian_query,
+ data={'image':image_data}, partial=True)
+ if serializer.is_valid():
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3017'], 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:
+ logging.error(e)
+ return custom_error_response(ERROR_CODE['2036'],response_status=status.HTTP_400_BAD_REQUEST)
+
+class ChangePasswordAPIView(views.APIView):
+ """
+ change password"
+ """
+ serializer_class = ChangePasswordSerializer
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ """
+ POST request to change current login user password
+ Payload
+ { "current_password":"Demo@123",
+ "new_password":"Demo@123"
+ }
+ """
+ serializer = ChangePasswordSerializer(
+ context=request.user,
+ data=request.data
+ )
+ if serializer.is_valid():
+ serializer.save()
+ return custom_response(
+ SUCCESS_CODE['3007'],
+ response_status=status.HTTP_200_OK
+ )
+ return custom_error_response(
+ serializer.errors,
+ response_status=status.HTTP_400_BAD_REQUEST
+ )
+
+class ResetPasswordAPIView(views.APIView):
+ """Reset password
+ Payload
+ {
+ "verification_code":"373770",
+ "password":"Demo@1323"
+ }"""
+ def post(self, request):
+ serializer = ResetPasswordSerializer(data=request.data)
+ if serializer.is_valid():
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3006'], response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+
+class ForgotPasswordAPIView(views.APIView):
+ """
+ Forgot password
+ """
+ serializer_class = ForgotPasswordSerializer
+
+ def post(self, request):
+
+ """
+ Payload
+ {
+ "email": "abc@yopmail.com"
+ }
+ """
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ email = serializer.validated_data['email']
+ # generate otp
+ verification_code = generate_otp()
+ # Send the verification code to the user's email
+ send_all_email.delay(
+ 'email_reset_verification.email', email, verification_code
+ )
+ expiry = 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
+ )
+
+class SendPhoneOtp(viewsets.ModelViewSet):
+ """Send otp on phone"""
+ serializer_class = UserPhoneOtpSerializer
+ def create(self, request, *args, **kwargs):
+ otp = generate_otp()
+ phone_number = self.request.data['phone']
+ if phone_number.isdigit() and len(phone_number) == 10:
+ phone_otp, created = UserPhoneOtp.objects.get_or_create(country_code=self.request.data['country_code'],
+ phone=self.request.data['phone'])
+ if phone_otp:
+ phone_otp.otp = otp
+ phone_otp.save()
+ return custom_response(None, {'phone_otp':otp}, response_status=status.HTTP_200_OK)
+ return custom_error_response(ERROR_CODE['2020'], response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class UserPhoneVerification(viewsets.ModelViewSet):
+ """Send otp on phone"""
+ serializer_class = UserPhoneOtpSerializer
+ def list(self, request, *args, **kwargs):
+ try:
+ phone_data = UserPhoneOtp.objects.filter(phone=self.request.GET.get('phone'),
+ otp=self.request.GET.get('otp')).last()
+ if phone_data:
+ phone_data.is_verified = True
+ phone_data.save()
+ return custom_response(SUCCESS_CODE['3012'], response_status=status.HTTP_200_OK)
+ else:
+ return custom_error_response(ERROR_CODE["2008"], response_status=status.HTTP_400_BAD_REQUEST)
+ except Exception:
+ return custom_error_response(ERROR_CODE["2008"], response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class UserLogin(viewsets.ViewSet):
+ """User login"""
+ @action(methods=['post'], detail=False)
+ def login(self, request):
+ username = request.data.get('username')
+ password = request.data.get('password')
+ user_type = request.META.get('HTTP_USER_TYPE')
+ device_id = request.META.get('HTTP_DEVICE_ID')
+ user = authenticate(request, username=username, password=password)
+
+ try:
+ if user is not None:
+ login(request, user)
+ if str(user_type) == USER_TYPE_FLAG["TWO"]:
+ guardian_data = Guardian.objects.filter(user__username=username).last()
+ if guardian_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).last()
+ if junior_data:
+ if junior_data.is_verified:
+ serializer = JuniorSerializer(
+ junior_data, context={'user_type': user_type}
+ ).data
+ else:
+ return custom_error_response(
+ ERROR_CODE["2071"],
+ response_status=status.HTTP_401_UNAUTHORIZED
+ )
+ else:
+ return custom_error_response(
+ ERROR_CODE["2069"],
+ response_status=status.HTTP_401_UNAUTHORIZED
+ )
+ # 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)
+ except Exception as e:
+ logging.error(e)
+ email_verified = UserEmailOtp.objects.filter(email=username).last()
+ refresh = RefreshToken.for_user(user)
+ access_token = str(refresh.access_token)
+ refresh_token = str(refresh)
+ data = {
+ "auth_token":access_token,
+ "refresh_token":refresh_token,
+ "is_profile_complete": False,
+ "user_type": user_type,
+ }
+ is_verified = False
+ if email_verified:
+ is_verified = email_verified.is_verified
+ if not is_verified:
+ otp = generate_otp()
+ email_verified.otp = otp
+ email_verified.save()
+ data.update({"email_otp":otp})
+ send_otp_email.delay(username, otp)
+ return custom_response(
+ ERROR_CODE['2024'],
+ {"email_otp": otp, "is_email_verified": is_verified},
+ response_status=status.HTTP_200_OK
+ )
+ data.update({"is_email_verified": is_verified})
+ return custom_response(
+ SUCCESS_CODE['3003'],
+ data,
+ response_status=status.HTTP_200_OK
+ )
+
+ @action(methods=['post'], detail=False)
+ def admin_login(self, request):
+ email = request.data.get('email')
+ password = request.data.get('password')
+ user = User.objects.filter(email__iexact=email, is_superuser=True
+ ).only('id', 'first_name', 'last_name', 'email',
+ 'username', 'is_active', 'is_superuser').first()
+
+ if not user or not user.check_password(password):
+ return custom_error_response(ERROR_CODE["2002"], response_status=status.HTTP_400_BAD_REQUEST)
+
+ serializer = SuperUserSerializer(user)
+ return custom_response(SUCCESS_CODE['3003'], serializer.data, response_status=status.HTTP_200_OK)
+
+
+class AdminLoginViewSet(viewsets.GenericViewSet):
+ """
+ admin login api
+ """
+ serializer_class = AdminLoginSerializer
+
+ @action(methods=['post'], url_name='login', url_path='login', detail=False)
+ def admin_login(self, request, *args, **kwargs):
+ """
+ :param request:
+ :return:
+ """
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ user = serializer.save()
+ refresh = RefreshToken.for_user(user)
+ access_token = str(refresh.access_token)
+ refresh_token = str(refresh)
+ data = {"auth_token": access_token, "refresh_token": refresh_token, "username": user.username,
+ "email": user.email, "first_name": user.first_name, "last_name": user.last_name,
+ "is_active": user.is_active, "user_type": '3', "is_superuser": user.is_superuser}
+ return custom_response(None, data)
+
+
+class UserEmailVerification(viewsets.ModelViewSet):
+ """User Email verification
+ Payload
+ {
+ "email":"ramu@yopmail.com",
+ "otp":"361123"
+ }"""
+ serializer_class = EmailVerificationSerializer
+ http_method_names = ('post',)
+
+ def create(self, request, *args, **kwargs):
+ try:
+ user_obj = User.objects.filter(username=self.request.data.get('email')).last()
+ email_data = UserEmailOtp.objects.filter(email=self.request.data.get('email'),
+ otp=self.request.data.get('otp')).last()
+ if email_data:
+ input_datetime_str = str(email_data.expired_at)
+ input_format = "%Y-%m-%d %H:%M:%S.%f%z"
+ output_format = "%Y-%m-%d %H:%M:%S.%f"
+ input_datetime = datetime.strptime(input_datetime_str, input_format)
+ output_datetime_str = input_datetime.strftime(output_format)
+ format_str = "%Y-%m-%d %H:%M:%S.%f"
+ datetime_obj = datetime.strptime(output_datetime_str, format_str)
+ if datetime.today() > datetime_obj:
+ return custom_error_response(ERROR_CODE["2029"], response_status=status.HTTP_400_BAD_REQUEST)
+ email_data.is_verified = True
+ email_data.save()
+ if email_data.user_type == '1':
+ junior_data = Junior.objects.filter(auth__email=self.request.data.get('email')).last()
+ if junior_data:
+ junior_data.is_verified = True
+ junior_data.save()
+ else:
+ guardian_data = Guardian.objects.filter(user__email=self.request.data.get('email')).last()
+ if guardian_data:
+ guardian_data.is_verified = True
+ guardian_data.save()
+ refresh = RefreshToken.for_user(user_obj)
+ access_token = str(refresh.access_token)
+ refresh_token = str(refresh)
+ return custom_response(SUCCESS_CODE['3011'], {"auth_token":access_token,
+ "refresh_token":refresh_token},
+ response_status=status.HTTP_200_OK)
+ else:
+ return custom_error_response(ERROR_CODE["2008"], response_status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ logging.error(e)
+ return custom_error_response(ERROR_CODE["2008"], response_status=status.HTTP_400_BAD_REQUEST)
+
+class ReSendEmailOtp(viewsets.ModelViewSet):
+ """Send otp on phone"""
+ serializer_class = EmailVerificationSerializer
+ http_method_names = ('post',)
+ def create(self, request, *args, **kwargs):
+ """Param
+ {"email":"ashok@yopmail.com"}
+ """
+ otp = generate_otp()
+ if User.objects.filter(email=request.data['email']):
+ expiry = timezone.now() + timezone.timedelta(days=1)
+ email_data, created = UserEmailOtp.objects.get_or_create(email=request.data['email'])
+ if created:
+ email_data.expired_at = expiry
+ email_data.save()
+ if email_data:
+ email_data.otp = otp
+ email_data.expired_at = expiry
+ email_data.save()
+ send_otp_email.delay(request.data['email'], otp)
+ return custom_response(SUCCESS_CODE['3016'], response_status=status.HTTP_200_OK)
+ else:
+ return custom_error_response(ERROR_CODE["2023"], response_status=status.HTTP_400_BAD_REQUEST)
+
+class ProfileAPIViewSet(viewsets.ModelViewSet):
+ """Profile viewset"""
+ serializer_class = JuniorProfileSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+
+ def list(self, request, *args, **kwargs):
+ """profile view
+ Params
+ user_type"""
+ user_type = request.META.get('HTTP_USER_TYPE')
+ if str(user_type) == '1':
+ junior_data = Junior.objects.filter(auth=self.request.user).last()
+ if junior_data:
+ serializer = JuniorProfileSerializer(junior_data)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ elif str(user_type) == '2':
+ guardian_data = Guardian.objects.filter(user=self.request.user).last()
+ if guardian_data:
+ serializer = GuardianProfileSerializer(guardian_data)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(None, response_status=status.HTTP_400_BAD_REQUEST)
+class UploadImageAPIViewSet(viewsets.ModelViewSet):
+ """upload task image"""
+ serializer_class = DefaultTaskImagesSerializer
+ http_method_names = ('post',)
+ def create(self, request, *args, **kwargs):
+ """upload images"""
+ image_data = request.data['image_url']
+ filename = f"default_task_images/{image_data.name}"
+ if image_data.size == NUMBER['zero']:
+ return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST)
+ image = upload_image_to_alibaba(image_data, filename)
+ image_data = image
+ request.data['image_url'] = image_data
+ serializer = DefaultTaskImagesSerializer(data=request.data)
+ if serializer.is_valid():
+ serializer.save()
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.error, response_status=status.HTTP_400_BAD_REQUEST)
+
+class DefaultImageAPIViewSet(viewsets.ModelViewSet):
+ """Profile viewset"""
+ serializer_class = DefaultTaskImagesDetailsSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+ def list(self, request, *args, **kwargs):
+ """profile view"""
+ queryset = DefaultTaskImages.objects.all()
+ serializer = DefaultTaskImagesSerializer(queryset, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+
+
+class DeleteUserProfileAPIViewSet(viewsets.GenericViewSet):
+ """ Delete user API view set
+ {"user_type":1,
+ "signup_method":"1",
+ "password":"Demo@123"}
+ signup_method 1 for manual
+ 2 for google login
+ 3 for apple login"""
+
+ @action(detail=False, methods=['POST'], url_path='user-account',serializer_class=UserDeleteSerializer,
+ permission_classes=[IsAuthenticated])
+ def account(self, request):
+ user_type = str(request.data['user_type'])
+ password = request.data.get('password')
+ signup_method = str(request.data.get('signup_method'))
+ serializer = self.get_serializer(data=request.data, context={'request': request, 'user': request.user,
+ 'user_type': user_type,
+ 'password': password,
+ 'signup_method':signup_method})
+ if serializer.is_valid():
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3005'], response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class UserNotificationAPIViewSet(viewsets.ModelViewSet):
+ """notification viewset"""
+ serializer_class = UserNotificationSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+ def list(self, request, *args, **kwargs):
+ """notification view"""
+ queryset = UserNotification.objects.filter(user=request.user)
+ serializer = UserNotificationSerializer(queryset, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+
+
+class UpdateUserNotificationAPIViewSet(viewsets.ModelViewSet):
+ """Update notification viewset"""
+ serializer_class = UpdateUserNotificationSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('post',)
+
+ def create(self, request, *args, **kwargs):
+ """Payload
+ {"email_notification": false,
+ "sms_notification": false,
+ "push_notification": false}
+ """
+ serializer = UpdateUserNotificationSerializer(data=request.data,
+ context=request.user)
+ if serializer.is_valid():
+ serializer.save()
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.error, response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class SendSupportEmail(views.APIView):
+ """support email api
+ payload
+ name
+ email
+ message
+ """
+ permission_classes = (IsAuthenticated,)
+
+ def post(self, request):
+ name = request.data.get('name')
+ sender = request.data.get('email')
+ message = request.data.get('message')
+ if name and sender and message:
+ try:
+ send_support_email(name, sender, message)
+ return custom_response(SUCCESS_CODE['3019'], response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+ else:
+ return custom_error_response(ERROR_CODE['2033'], response_status=status.HTTP_400_BAD_REQUEST)
+
+class LogoutAPIView(views.APIView):
+ """Log out API"""
+ permission_classes = (IsAuthenticated,)
+
+ def post(self, request):
+ remove_fcm_token(
+ request.auth.payload['user_id'],
+ request.META['HTTP_AUTHORIZATION'].split(" ")[1],
+ request.data.get('registration_id', ""))
+ logout(request)
+ request.session.flush()
+ return custom_response(SUCCESS_CODE['3020'], response_status=status.HTTP_200_OK)
+
+
+class AccessTokenAPIView(views.APIView):
+ """generate access token API"""
+
+ def post(self, request):
+ # Assuming you have a refresh_token string
+ refresh_token = request.data['refresh_token']
+ # Create a RefreshToken instance from the refresh token string
+ refresh = RefreshToken(refresh_token)
+ # Generate a new access token
+ access_token = str(refresh.access_token)
+ data = {"auth_token": access_token}
+ return custom_response(None, data, response_status=status.HTTP_200_OK)
+
+
+class ForceUpdateViewSet(GenericViewSet, mixins.CreateModelMixin):
+ """FAQ view set"""
+
+ serializer_class = ForceUpdateSerializer
+ http_method_names = ['post']
+
+
+ def create(self, request, *args, **kwargs):
+ """
+ faq create api method
+ :param request:
+ :param args: version, device type
+ :param kwargs:
+ :return: success message
+ """
+ if ForceUpdate.objects.all().count() >= 2:
+ return custom_error_response(ERROR_CODE['2080'], response_status=status.HTTP_400_BAD_REQUEST)
+ obj_data = [ForceUpdate(**item) for item in request.data]
+ try:
+ ForceUpdate.objects.bulk_create(obj_data)
+ return custom_response(SUCCESS_CODE["3046"], response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
diff --git a/base/__init__.py b/base/__init__.py
new file mode 100644
index 0000000..860fa73
--- /dev/null
+++ b/base/__init__.py
@@ -0,0 +1,3 @@
+"""
+This is init module of the Project Zod Bank
+"""
diff --git a/base/common_email.py b/base/common_email.py
new file mode 100644
index 0000000..4eb2a36
--- /dev/null
+++ b/base/common_email.py
@@ -0,0 +1,30 @@
+"""
+Common send_mail function
+"""
+import logging
+
+from django.core.mail import EmailMultiAlternatives
+
+
+def send_mail(subject, message, from_email, recipient_list, html_message=None, cc=None,
+ fail_silently=False):
+ """
+ Send Email
+ :param subject:
+ :param message:
+ :param from_email:
+ :param recipient_list:
+ :param html_message:
+ :param cc:
+ :param fail_silently:
+ :return:
+ """
+ try:
+ mail = EmailMultiAlternatives(subject, message, from_email, recipient_list, cc)
+ if html_message:
+ mail.attach_alternative(html_message, 'text/html')
+
+ return mail.send(fail_silently)
+ except Exception as e:
+ logging.error(e)
+ return False
diff --git a/base/constants.py b/base/constants.py
new file mode 100644
index 0000000..da203ae
--- /dev/null
+++ b/base/constants.py
@@ -0,0 +1,137 @@
+"""
+This module contains constants used throughout the project
+"""
+import os
+
+# GOOGLE_URL used for interact with google server to verify user existence.
+#GOOGLE_URL = "https://www.googleapis.com/plus/v1/"
+
+# Define Code prefix word
+# for guardian code,
+# junior code,
+# referral code"""
+ZOD = 'ZOD'
+JUN = 'JUN'
+GRD = 'GRD'
+# Define number variable
+# from zero to
+# twenty and
+# some standard
+# number"""
+NUMBER = {
+ 'point_zero': 0.0, 'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7,
+ 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, 'thirteen': 13, 'fourteen': 14, 'fifteen': 15,
+ 'sixteen': 16, 'seventeen': 17, 'eighteen': 18, 'nineteen': 19, 'twenty': 20,
+ 'twenty_one': 21, 'twenty_two': 22,'twenty_three': 23, 'twenty_four': 24, 'twenty_five': 25,
+ 'thirty': 30, 'forty': 40, 'fifty': 50, 'sixty': 60, 'seventy': 70, 'eighty': 80, 'ninty': 90,
+ 'ninety_nine': 99, 'hundred': 100, 'thirty_six_hundred': 3600
+}
+
+none = "none"
+
+# Super Admin string constant for 'role'
+SUPER_ADMIN = "Super Admin"
+
+# Define jwt_token_expiration time in minutes for now token will expire after 3 days
+JWT_TOKEN_EXPIRATION = 3 * 24 * 60
+
+# Define common file extention
+FILE_EXTENSION = ("gif", "jpeg", "jpg", "png", "svg")
+
+# Define file size in bytes(5MB = 5 * 1024 * 1024)
+FILE_SIZE = 5 * 1024 * 1024
+
+# String constant for configurable date for allocation lock period
+ALLOCATION_LOCK_DATE = 1
+# guardian code status tuple
+guardian_code_tuple = ('1','3')
+"""user type"""
+USER_TYPE = (
+ ('1', 'junior'),
+ ('2', 'guardian'),
+ ('3', 'superuser')
+)
+DEVICE_TYPE = (
+ ('1', 'android'),
+ ('2', 'ios')
+)
+USER_TYPE_FLAG = {
+ "FIRST" : "1",
+ "TWO" : "2",
+ "THREE": "3"
+}
+
+"""gender"""
+GENDERS = (
+ ('1', 'Male'),
+ ('2', 'Female')
+)
+# Task status"""
+TASK_STATUS = (
+ ('1', 'pending'),
+ ('2', 'in-progress'),
+ ('3', 'rejected'),
+ ('4', 'requested'),
+ ('5', 'completed'),
+ ('6', 'expired')
+)
+# sign up method
+SIGNUP_METHODS = (
+ ('1', 'manual'),
+ ('2', 'google'),
+ ('3', 'apple')
+)
+# guardian code status
+GUARDIAN_CODE_STATUS = (
+ ('1', 'no guardian code'),
+ ('2', 'exist guardian code'),
+ ('3', 'request for guardian code')
+)
+# article status
+ARTICLE_STATUS = (
+ ('1', 'read'),
+ ('2', 'in_progress'),
+ ('3', 'completed')
+)
+# relationship
+RELATIONSHIP = (
+ ('1', 'parent'),
+ ('2', 'legal_guardian')
+)
+"""
+Define task status
+in a number"""
+PENDING = 1
+IN_PROGRESS = 2
+REJECTED = 3
+REQUESTED = 4
+COMPLETED = 5
+EXPIRED = 6
+TASK_POINTS = 5
+# duplicate name used defined in constant PROJECT_NAME
+PROJECT_NAME = 'Zod Bank'
+# define user type constant
+GUARDIAN = 'guardian'
+JUNIOR = 'junior'
+SUPERUSER = 'superuser'
+# numbers used as a constant
+
+# Define the byte into kb
+BYTE_IMAGE_SIZE = 1024
+
+# validate file size
+MAX_FILE_SIZE = 1024 * 1024 * 5
+
+ARTICLE_SURVEY_POINTS = 5
+MAX_ARTICLE_CARD = 6
+
+# min and max survey
+MIN_ARTICLE_SURVEY = 5
+MAX_ARTICLE_SURVEY = 10
+
+# already register
+Already_register_user = "duplicate key value violates unique constraint"
+
+ARTICLE_CARD_IMAGE_FOLDER = 'article-card-images'
+
+DATE_FORMAT = '%Y-%m-%d'
diff --git a/base/image_constants.py b/base/image_constants.py
new file mode 100644
index 0000000..0737698
--- /dev/null
+++ b/base/image_constants.py
@@ -0,0 +1,16 @@
+"""
+This module contains constants used throughout the project
+"""
+from zod_bank.settings import BUCKET_NAME
+
+# Define S3 folder url
+S3_FOLDER_DIR = {
+ 'user_image': 'user_image/',
+}
+
+# S3 bucket url
+S3_URL = "https://"+BUCKET_NAME+".s3.amazonaws.com/"
+
+S3_FOLDER_URL = {
+ 'user_image_file': S3_URL+S3_FOLDER_DIR['user_image'],
+}
diff --git a/base/messages.py b/base/messages.py
new file mode 100644
index 0000000..3977895
--- /dev/null
+++ b/base/messages.py
@@ -0,0 +1,202 @@
+"""
+This module contains all the messages used all across the project
+"""
+
+ERROR_CODE_REQUIRED = {
+ # Error code for email address
+ "1000": ["Required email address not found."],
+ # Error code for password
+ "1001": ["Required password not found."],
+ # Error code for Required Post parameters
+ "1002": ["Required POST parameters not found."],
+ # Error code for Required Get parameters
+ "1003": ["Required GET parameters not found."],
+ # Error code for Required Headers
+ "1004": ["Required headers were not found."],
+ # Error code for Required Put parameters
+ "1005": ["Required PUT parameters not found."],
+ # Error code for Required query parameters
+ "1006": ["Required query parameters is not valid."],
+ # Error code for Required Head parameters
+ "1008": ["Required HEAD parameters not found."]
+}
+
+# Error code
+ERROR_CODE = {
+ "2000": "Invalid email address. Please enter a registered email.",
+ "2001": "This is your existing password. Please choose other one",
+ "2002": "Invalid username or password.",
+ "2003": "An account already exists with this email address.",
+ "2004": "User not found.",
+ "2005": "Your account has been activated.",
+ "2006": "Your account is not activated.",
+ "2007": "Your account already activated.",
+ "2008": "The OTP entered is not correct.",
+ "2009": "The user provided cannot be found or the reset password token has become invalid/timed out.",
+ "2010": "Invalid Link.",
+ "2011": "Your profile has not been completed yet.",
+ "2012": "Phone number already used",
+ "2013": "Invalid token.",
+ "2014": "Your old password doesn't match.",
+ "2015": "Invalid old password.",
+ "2016": "Invalid search.",
+ "2017": "{model} object with {pk} does not exist",
+ "2018": "Attached File not found",
+ "2019": "Invalid Referral code",
+ "2020": "Enter valid mobile number",
+ "2021": "User registered",
+ "2022": "Invalid Guardian code",
+ "2023": "Invalid user",
+ # email not verified
+ "2024": "Email not verified",
+ "2025": "Invalid input. Expected a list of strings.",
+ # check old and new password
+ "2026": "New password should not same as old password",
+ "2027": "data should contain `identityToken`",
+ "2028": "You are not authorized person to sign up on this platform",
+ "2029": "Validity of otp verification has expired. Please request a new one.",
+ "2030": "Use correct user type and token",
+ # invalid password
+ "2031": "Invalid password",
+ "2032": "Failed to send email",
+ "2033": "Missing required fields",
+ "2034": "Junior is not associated",
+ # image size
+ "2035": "Image should not be 0 kb",
+ "2036": "Choose valid user",
+ # log in multiple device msg
+ "2037": "You are already log in another device",
+ "2038": "Choose valid action for task",
+ # card length limit
+ "2039": "Add at least one article card or maximum 6",
+ "2040": "Add at least 5 article survey or maximum 10",
+ # add article msg
+ "2041": "Article with given id doesn't exist.",
+ "2042": "Article Card with given id doesn't exist.",
+ "2043": "Article Survey with given id doesn't exist.",
+ "2044": "Task does not exist",
+ "2045": "Invalid guardian",
+ # past due date
+ "2046": "Due date must be future date",
+ # invalid junior id msg
+ "2047": "Invalid Junior ID ",
+ "2048": "Choose right file for image",
+ # task request
+ "2049": "This task is already requested ",
+ "2059": "Already exist junior",
+ # task status
+ "2060": "Task does not exist or not in pending state",
+ "2061": "Please insert image or check the image is valid or not.",
+ # email not null
+ "2062": "Please enter email address",
+ "2063": "Unauthorized access.",
+ "2064": "To change your password first request an OTP and get it verify then change your password.",
+ "2065": "Passwords do not match. Please try again.",
+ "2066": "Task does not exist or not in expired state",
+ "2067": "Action not allowed. User type missing.",
+ "2068": "No guardian associated with this junior",
+ "2069": "Invalid user type",
+ "2070": "You are not registered as a guardian in our system. Please try again as junior.",
+ "2071": "You are not registered as a junior in our system. Please try again as guardian.",
+ "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 associated with you",
+ "2077": "You can not add guardian",
+ "2078": "This junior is not associated with you",
+ # force update
+ "2079": "Please update your app version for enjoying uninterrupted services",
+ "2080": "Can not add App version",
+ "2081": "A junior can only be associated with a maximum of 3 guardian",
+ # guardian code not exist
+ "2082": "Guardian code does not exist",
+ "2083": "You can not start this task because guardian is not associate with you",
+ "2084": "You can not complete this task because guardian is not associate with you",
+ "2085": "You can not take action on this task because junior is not associate with you"
+
+}
+"""Success message code"""
+SUCCESS_CODE = {
+ "3000": "ok",
+ # Success code for password
+ "3001": "Sign up successfully",
+ # Success code for Thank you
+ "3002": "Thank you for contacting us! Our Consumer Experience Team will reach out to you shortly.",
+ # Success code for account activation
+ "3003": "Log in successful.",
+ # Success code for password reset
+ "3004": "Password reset link has been sent to your email address.",
+ # Success code for link verified
+ "3005": "Your account has been deleted successfully.",
+ # Success code for password reset
+ "3006": "Password reset successful. You can now log in with your new password.",
+ # Success code for password update
+ "3007": "Your password has been changed successfully.",
+ # Success code for valid link
+ "3008": "You have a valid link.",
+ # Success code for logged out
+ "3009": "You have successfully logged out.",
+ # Success code for check all fields
+ "3010": "All fields are valid.",
+ "3011": "Email OTP has been verified successfully.",
+ "3012": "Phone OTP has been verified successfully.",
+ "3013": "Valid Guardian code",
+ "3014": "Password has been updated successfully.",
+ "3015": "Verification code has been sent on your email.",
+ "3016": "An OTP has been sent on your email.",
+ "3017": "Profile image update successfully",
+ "3018": "Task created successfully",
+ "3019": "Support Email sent successfully",
+ "3020": "Logged out successfully.",
+ "3021": "Junior has been added successfully.",
+ "3022": "Junior has been removed successfully.",
+ "3023": "Junior has been approved successfully.",
+ "3024": "Junior request is rejected successfully.",
+ "3025": "Task is approved successfully.",
+ "3026": "Task is rejected successfully.",
+ "3027": "Article has been created successfully.",
+ "3028": "Article has been updated successfully.",
+ "3029": "Article has been deleted successfully.",
+ "3030": "Article Card has been removed successfully.",
+ "3031": "Article Survey has been removed successfully.",
+ "3032": "Task request sent successfully.",
+ "3033": "Valid Referral code.",
+ "3034": "Invite guardian successfully.",
+ "3035": "Task started successfully.",
+ "3036": "Task reassign successfully.",
+ "3037": "Profile has been updated successfully.",
+ "3038": "Status has been changed successfully.",
+ # notification read
+ "3039": "Notification read successfully.",
+ # start article
+ "3040": "Start article successfully.",
+ # complete article
+ "3041": "Article completed successfully.",
+ # submit assessment successfully
+ "3042": "Assessment completed successfully.",
+ # read article
+ "3043": "Read article card successfully.",
+ # remove guardian code request
+ "3044": "Remove guardian code request successfully.",
+ # create faq
+ "3045": "Create FAQ data.",
+ "3046": "Add App version successfully."
+
+}
+"""status code error"""
+STATUS_CODE_ERROR = {
+ # Status code for Invalid Input
+ "4001": ["Invalid input."],
+ # Status code for Authentication credentials
+ "4002": ["Authentication credentials were not provided."],
+ # Status code for Permission
+ "4003": ["You do not have permission to perform this action."],
+ # Status code for not found
+ "4004": ["Not found."],
+ # Status code for method not allowed
+ "4005": ["Method not allowed."]
+}
+
+
diff --git a/base/pagination.py b/base/pagination.py
new file mode 100644
index 0000000..90b5c95
--- /dev/null
+++ b/base/pagination.py
@@ -0,0 +1,34 @@
+"""
+web_admin pagination file
+"""
+# third party imports
+from collections import OrderedDict
+from rest_framework.pagination import PageNumberPagination
+
+from account.utils import custom_response
+from base.constants import NUMBER
+
+
+class CustomPageNumberPagination(PageNumberPagination):
+ """
+ custom paginator class
+ """
+ # Set the desired page size
+ page_size = NUMBER['ten']
+ page_size_query_param = 'page_size'
+ # Set a maximum page size if needed
+ max_page_size = NUMBER['hundred']
+
+ def get_paginated_response(self, data):
+ """
+ :param data: queryset to be paginated
+ :return: return a OrderedDict
+ """
+ return custom_response(None, OrderedDict([
+ ('count', self.page.paginator.count),
+ ('data', data),
+ ('current_page', self.page.number),
+ ('total_pages', self.page.paginator.num_pages),
+
+
+ ]))
diff --git a/base/routers.py b/base/routers.py
new file mode 100644
index 0000000..e2df0e1
--- /dev/null
+++ b/base/routers.py
@@ -0,0 +1,17 @@
+"""
+Custom routers for job sourcing .
+"""
+# third party imports
+from rest_framework.routers import DefaultRouter
+
+
+class OptionalSlashRouter(DefaultRouter):
+ """
+ optional slash router class
+ """
+ def __init__(self):
+ """
+ explicitly appending '/' in urls if '/' doesn't exists for making common url patterns .
+ """
+ super(OptionalSlashRouter, self).__init__()
+ self.trailing_slash = '/?'
diff --git a/base/tasks.py b/base/tasks.py
new file mode 100644
index 0000000..edd3dd1
--- /dev/null
+++ b/base/tasks.py
@@ -0,0 +1,89 @@
+"""
+web_admin tasks file
+"""
+import datetime
+
+# third party imports
+from celery import shared_task
+from templated_email import send_templated_mail
+
+# django imports
+from django.conf import settings
+from django.db.models import F, Window
+from django.db.models.functions.window import Rank
+
+# local imports
+from base.constants import PENDING, IN_PROGRESS, JUNIOR, GUARDIAN
+from guardian.models import JuniorTask
+from junior.models import JuniorPoints
+from notifications.constants import PENDING_TASK_EXPIRING, IN_PROGRESS_TASK_EXPIRING, NOTIFICATION_DICT, TOP_JUNIOR
+from notifications.models import Notification
+from notifications.utils import send_notification, get_from_user_details, send_notification_multiple_user
+
+
+@shared_task
+def send_email(recipient_list, template, context: dict = None):
+ """
+ used to send otp on email
+ :param context:
+ :param recipient_list: e-mail list
+ :param template: email template
+ """
+ if context is None:
+ context = {}
+ from_email = settings.EMAIL_FROM_ADDRESS
+ send_templated_mail(
+ template_name=template,
+ from_email=from_email,
+ recipient_list=recipient_list,
+ context=context
+ )
+ return True
+
+
+@shared_task()
+def notify_task_expiry():
+ """
+ task to send notification for those task which expiring soon
+ :return:
+ """
+ all_pending_tasks = JuniorTask.objects.filter(
+ junior__is_verified=True,
+ task_status__in=[PENDING, IN_PROGRESS],
+ due_date__range=[datetime.datetime.now().date(),
+ (datetime.datetime.now().date() + datetime.timedelta(days=1))])
+ if pending_tasks := all_pending_tasks.filter(task_status=PENDING):
+ for task in pending_tasks:
+ send_notification(PENDING_TASK_EXPIRING, task.guardian.user_id, GUARDIAN, task.junior.auth_id,
+ {'task_id': task.id})
+ if in_progress_tasks := all_pending_tasks.filter(task_status=IN_PROGRESS):
+ for task in in_progress_tasks:
+ send_notification(IN_PROGRESS_TASK_EXPIRING, task.junior.auth_id, JUNIOR, task.guardian.user_id,
+ {'task_id': task.id})
+ return True
+
+
+@shared_task()
+def notify_top_junior():
+ """
+ task to send notification for top leaderboard junior to all junior's
+ :return:
+ """
+ junior_points_qs = JuniorPoints.objects.filter(
+ junior__is_verified=True
+ ).select_related(
+ 'junior', 'junior__auth'
+ ).annotate(rank=Window(
+ expression=Rank(),
+ order_by=[F('total_points').desc(), 'junior__created_at'])
+ ).order_by('-total_points', 'junior__created_at')
+
+ prev_top_position = junior_points_qs.filter(position=1).first()
+ new_top_position = junior_points_qs.filter(rank=1).first()
+ if prev_top_position != new_top_position:
+ send_notification_multiple_user(TOP_JUNIOR, new_top_position.junior.auth_id, JUNIOR,
+ {'points': new_top_position.total_points})
+ for junior_point in junior_points_qs:
+ junior_point.position = junior_point.rank
+ junior_point.save()
+ return True
diff --git a/base/upload_file.py b/base/upload_file.py
new file mode 100644
index 0000000..2d47ad2
--- /dev/null
+++ b/base/upload_file.py
@@ -0,0 +1,177 @@
+"""
+This file used for file uploaded
+"""
+import datetime
+# python imports
+import logging
+import mimetypes
+import os
+
+import boto3
+from django.core.files.storage import FileSystemStorage
+# django imports
+from django.utils.crypto import get_random_string
+from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_200_OK
+
+from base import constants
+from base.constants import NUMBER
+# local import
+from zod_bank.settings import base_settings as settings
+from zod_bank.settings.base_settings import BASE_DIR
+
+
+def image_upload(folder, file_name, data):
+ """
+ Function to upload files
+ :param folder:folder location string
+ :param file_name:file_name without ext string
+ :param data:data file obj
+ :return:Dictionary
+ """
+ status = HTTP_400_BAD_REQUEST
+ img_name = None
+ error = None
+ try:
+ s3_client = boto3.client('s3',
+ aws_access_key_id=settings.AWS_ACCESS_KEY,
+ aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
+ region_name=settings.AWS_DEFAULT_REGION
+ )
+
+ bucket_name = settings.BUCKET_NAME
+ MEDIA_ROOT = os.path.join(BASE_DIR, 'media/tmp')
+ fss = FileSystemStorage()
+ file = fss.save('tmp/' + str(file_name), data)
+ fss.url(file)
+ tmp_file = os.path.join(MEDIA_ROOT, str(file_name))
+ s3_client.upload_file(
+ tmp_file, bucket_name, folder + str(file_name),
+ ExtraArgs={'ACL': 'public-read', 'ContentType': data.content_type}
+ )
+ os.unlink(tmp_file)
+ img_name = file_name
+ status = HTTP_200_OK
+ except Exception as e:
+ error = e
+ logging.error(e)
+ return status, error, img_name
+
+
+def file_delete(folder, file_name):
+ """
+ To delete common file
+ :param folder: folder name str
+ :param file_name: file_name string type
+ """
+ status = HTTP_400_BAD_REQUEST
+ error = None
+ try:
+ s3_client = boto3.client('s3',
+ aws_access_key_id=settings.AWS_ACCESS_KEY,
+ aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
+ region_name=settings.AWS_DEFAULT_REGION
+ )
+
+ s3_client.delete_object(Bucket=settings.BUCKET_NAME, Key=str(folder) + str(file_name))
+ status = HTTP_200_OK
+ except Exception as e:
+ error = e
+ return status, error
+
+
+def get_aws_obj(folder, file_name):
+ """
+ To get aws file obj
+ :param folder: folder string type
+ :param file_name: file_name string type
+ """
+ status = HTTP_400_BAD_REQUEST
+ obj = None
+ try:
+ s3_client = boto3.client('s3',
+ aws_access_key_id=settings.AWS_ACCESS_KEY,
+ aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
+ region_name=settings.AWS_DEFAULT_REGION
+ )
+ file_name = folder + file_name
+ obj = s3_client.get_object(Bucket=settings.BUCKET_NAME, Key=file_name)
+ status = HTTP_200_OK
+ except Exception as e:
+ logging.error(e)
+ return status, obj
+
+
+def upload_image(post_data, folder):
+ """
+ :param post_data:
+ :param folder: string type
+ :return:
+ """
+ upload_obj = None
+ # Check Post data
+ if post_data:
+ date_now = datetime.datetime.now()
+ file_extension = os.path.splitext(str(post_data.name))
+ file_extension = file_extension[constants.NUMBER['one']].split(".")[constants.NUMBER['one']].lower()
+ rand = get_random_string(NUMBER['twelve'])
+ image_name = str(rand) + date_now.strftime("%s") + "." + file_extension
+ upload_obj = image_upload(folder, image_name, post_data)
+ return upload_obj
+
+
+def upload_voice_kit_image(post_data, folder, image_dir):
+ """
+ :param post_data:
+ :param folder: string type
+ :param image_dir: image_dir
+ :return:
+ """
+ upload_obj = None
+ # Check Post data
+ if post_data:
+ date_now = datetime.datetime.now()
+ file_extension = os.path.splitext(str(post_data))
+ file_extension = file_extension[constants.NUMBER['one']].split(".")[constants.NUMBER['one']].lower()
+ rand = get_random_string(NUMBER['twelve'])
+ image_name = str(rand) + date_now.strftime("%s") + "." + file_extension
+ upload_obj = voice_kit_image_upload(folder, image_name, post_data, image_dir)
+ return upload_obj
+
+
+def voice_kit_image_upload(folder, file_name, data, image_dir):
+ """
+ Function to upload files
+ :param folder:folder location string
+ :param file_name:file_name without ext string
+ :param data:data file obj
+ :return:Dictionary
+ """
+ status = HTTP_400_BAD_REQUEST
+ img_name = None
+ error = None
+ try:
+ s3_client = boto3.client('s3',
+ aws_access_key_id=settings.AWS_ACCESS_KEY,
+ aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
+ region_name=settings.AWS_DEFAULT_REGION
+ )
+
+ bucket_name = settings.BUCKET_NAME
+ MEDIA_ROOT = os.path.join(BASE_DIR, 'media/tmp')
+ fss = FileSystemStorage()
+ with open(image_dir+data, 'rb') as f:
+ file = fss.save('tmp/' + str(file_name), f)
+ fss.url(file)
+ tmp_file = os.path.join(MEDIA_ROOT, str(file_name))
+ s3_client.upload_file(
+ tmp_file, bucket_name, folder + str(file_name),
+ ExtraArgs={'ACL': 'public-read', 'ContentType': mimetypes.guess_type(file_name)[0]}
+ )
+ os.unlink(tmp_file)
+ img_name = file_name
+ status = HTTP_200_OK
+ except Exception as e:
+ error = e
+ logging.error(e)
+ return status, error, img_name
+
diff --git a/celerybeat-schedule b/celerybeat-schedule
new file mode 100644
index 0000000..2a48515
Binary files /dev/null and b/celerybeat-schedule differ
diff --git a/coverage-reports/coverage.xml b/coverage-reports/coverage.xml
new file mode 100644
index 0000000..f16234c
--- /dev/null
+++ b/coverage-reports/coverage.xml
@@ -0,0 +1,5792 @@
+
+
+
+
+
+ .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml
new file mode 100644
index 0000000..436833b
--- /dev/null
+++ b/docker-compose-prod.yml
@@ -0,0 +1,39 @@
+version: '3'
+services:
+ nginx:
+ image: nginx:latest
+ container_name: nginx
+ restart: always
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./nginx:/etc/nginx/conf.d
+ - .:/usr/src/app
+ depends_on:
+ - web
+ web:
+ build: .
+ container_name: prod_django
+ restart: always
+ command: bash -c "pip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrate && gunicorn zod_bank.wsgi -b 0.0.0.0:8000 -t 300 --log-level=info"
+ volumes:
+ - .:/usr/src/app
+
+ broker:
+ image: rabbitmq:3.7
+ container_name: prod_rabbitmq
+ volumes:
+ - .:/usr/src/app
+ ports:
+ - 5673:5673
+
+ worker:
+ build: .
+ image: celery
+ container_name: prod_celery
+ restart: "always"
+ command: bash -c " celery -A zod_bank.celery worker --concurrency=1 -B -l DEBUG -E"
+ volumes:
+ - .:/usr/src/app
+ depends_on:
+ - broker
diff --git a/docker-compose-qa.yml b/docker-compose-qa.yml
new file mode 100644
index 0000000..6e3f4d5
--- /dev/null
+++ b/docker-compose-qa.yml
@@ -0,0 +1,39 @@
+version: '3'
+services:
+ nginx:
+ image: nginx:latest
+ container_name: nginx
+ restart: always
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./nginx:/etc/nginx/conf.d
+ - .:/usr/src/app
+ depends_on:
+ - web
+ web:
+ build: .
+ container_name: qa_django
+ restart: always
+ command: bash -c "pip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrate && gunicorn zod_bank.wsgi -b 0.0.0.0:8000 -t 300 --log-level=info"
+ volumes:
+ - .:/usr/src/app
+
+ broker:
+ image: rabbitmq:3.7
+ container_name: qa_rabbitmq
+ volumes:
+ - .:/usr/src/app
+ ports:
+ - 5673:5673
+
+ worker:
+ build: .
+ image: celery
+ container_name: qa_celery
+ restart: "always"
+ command: bash -c " celery -A zod_bank.celery worker --concurrency=1 -B -l DEBUG -E"
+ volumes:
+ - .:/usr/src/app
+ depends_on:
+ - broker
diff --git a/docker-compose-stage.yml b/docker-compose-stage.yml
new file mode 100644
index 0000000..39db221
--- /dev/null
+++ b/docker-compose-stage.yml
@@ -0,0 +1,39 @@
+version: '3'
+services:
+ nginx:
+ image: nginx:latest
+ container_name: nginx
+ restart: always
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./nginx:/etc/nginx/conf.d
+ - .:/usr/src/app
+ depends_on:
+ - web
+ web:
+ build: .
+ container_name: stage_django
+ restart: always
+ command: bash -c "pip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrate && gunicorn zod_bank.wsgi -b 0.0.0.0:8000 -t 300 --log-level=info"
+ volumes:
+ - .:/usr/src/app
+
+ broker:
+ image: rabbitmq:3.7
+ container_name: stage_rabbitmq
+ volumes:
+ - .:/usr/src/app
+ ports:
+ - 5673:5673
+
+ worker:
+ build: .
+ image: celery
+ container_name: stage_celery
+ restart: "always"
+ command: bash -c " celery -A zod_bank.celery worker --concurrency=1 -B -l DEBUG -E"
+ volumes:
+ - .:/usr/src/app
+ depends_on:
+ - broker
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..edd309d
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,39 @@
+version: '3'
+services:
+ nginx:
+ image: nginx:latest
+ container_name: nginx
+ restart: always
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./nginx:/etc/nginx/conf.d
+ - .:/usr/src/app
+ depends_on:
+ - web
+ web:
+ build: .
+ container_name: django
+ restart: always
+ command: bash -c "pip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrate && gunicorn zod_bank.wsgi -b 0.0.0.0:8000 -t 300 --log-level=info"
+ volumes:
+ - .:/usr/src/app
+
+ broker:
+ image: rabbitmq:3.7
+ container_name: rabbitmq
+ volumes:
+ - .:/usr/src/app
+ ports:
+ - 5673:5673
+
+ worker:
+ build: .
+ image: celery
+ container_name: dev_celery
+ restart: "always"
+ command: bash -c " celery -A zod_bank.celery worker --concurrency=1 -B -l DEBUG -E"
+ volumes:
+ - .:/usr/src/app
+ depends_on:
+ - broker
diff --git a/fixtures/faq.json b/fixtures/faq.json
new file mode 100644
index 0000000..3221c7e
--- /dev/null
+++ b/fixtures/faq.json
@@ -0,0 +1,112 @@
+[
+ {
+ "model": "junior.faq",
+ "pk": 1,
+ "fields": {
+ "question": "What is ZOD ?",
+ "description": "We are a future neobank for under 18. We aim to provide children with the ability to use debit cards under the watchfull eye of their parents.",
+ "status": 1,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ },
+ {
+ "model": "junior.faq",
+ "pk": 2,
+ "fields": {
+ "question": "What is financial literacy ?",
+ "description": "",
+ "status": 2,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ },
+ {
+ "model": "junior.faq",
+ "pk": 3,
+ "fields": {
+ "question": "How can we win with Zod ?",
+ "description": "",
+ "status": 2,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ },
+ {
+ "model": "junior.faq",
+ "pk": 4,
+ "fields": {
+ "question": "What is a budget ?",
+ "description": "",
+ "status": 2,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ },
+ {
+ "model": "junior.faq",
+ "pk": 5,
+ "fields": {
+ "question": "What is the difference between stocks and bonds ?",
+ "description": "",
+ "status": 2,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ },
+ {
+ "model": "junior.faq",
+ "pk": 6,
+ "fields": {
+ "question": "What is compound interest ?",
+ "description": "",
+ "status": 2,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ },
+ {
+ "model": "junior.faq",
+ "pk": 7,
+ "fields": {
+ "question": "What is diversification ?",
+ "description": "",
+ "status": 2,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ },
+ {
+ "model": "junior.faq",
+ "pk": 8,
+ "fields": {
+ "question": "What is a 401(k) ?",
+ "description": "",
+ "status": 2,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ },
+ {
+ "model": "junior.faq",
+ "pk": 9,
+ "fields": {
+ "question": "What is an emergency fund ?",
+ "description": "",
+ "status": 2,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ },
+ {
+ "model": "junior.faq",
+ "pk": 10,
+ "fields": {
+ "question": "What is a mortgage ?",
+ "description": "",
+ "status": 2,
+ "created_at": "2023-11-08T12:32:55.291Z",
+ "updated_at": "2023-11-08T12:32:55.291Z"
+ }
+ }
+]
diff --git a/guardian/__init__.py b/guardian/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/guardian/admin.py b/guardian/admin.py
new file mode 100644
index 0000000..97edbdb
--- /dev/null
+++ b/guardian/admin.py
@@ -0,0 +1,23 @@
+"""Guardian admin"""
+"""Third party Django app"""
+from django.contrib import admin
+"""Import Django app"""
+from .models import Guardian, JuniorTask
+# Register your models here.
+@admin.register(Guardian)
+class GuardianAdmin(admin.ModelAdmin):
+ """Junior Admin"""
+ list_display = ['user', 'family_name']
+
+ def __str__(self):
+ """Return email id"""
+ return self.user__email
+
+@admin.register(JuniorTask)
+class TaskAdmin(admin.ModelAdmin):
+ """Junior Admin"""
+ list_display = ['id', 'task_name', 'task_status', 'junior', 'due_date', 'points', 'created_at', 'updated_at']
+
+ def __str__(self):
+ """Return email id"""
+ return str(self.task_name) + str(self.points)
diff --git a/guardian/apps.py b/guardian/apps.py
new file mode 100644
index 0000000..fcaf209
--- /dev/null
+++ b/guardian/apps.py
@@ -0,0 +1,9 @@
+"""Guardian app file"""
+"""Third party Django app"""
+from django.apps import AppConfig
+
+
+class CustodianConfig(AppConfig):
+ """Guardian config"""
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'guardian'
diff --git a/guardian/migrations/0001_initial.py b/guardian/migrations/0001_initial.py
new file mode 100644
index 0000000..0038a3b
--- /dev/null
+++ b/guardian/migrations/0001_initial.py
@@ -0,0 +1,43 @@
+# Generated by Django 4.2.2 on 2023-06-23 12:05
+
+from django.conf import settings
+import django.contrib.postgres.fields
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Guardian',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('country_code', models.IntegerField(blank=True, null=True)),
+ ('phone', models.CharField(blank=True, default=None, max_length=31, null=True)),
+ ('family_name', models.CharField(blank=True, default=None, max_length=50, null=True)),
+ ('gender', models.CharField(blank=True, choices=[('1', 'Male'), ('2', 'Female')], default=None, max_length=15, null=True)),
+ ('dob', models.DateField(blank=True, default=None, max_length=15, null=True)),
+ ('guardian_code', models.CharField(blank=True, default=None, max_length=10, null=True)),
+ ('junior_code', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, default=None, max_length=10, null=True), null=True, size=None)),
+ ('referral_code', models.CharField(blank=True, default=None, max_length=10, null=True)),
+ ('referral_code_used', models.CharField(blank=True, default=None, max_length=10, null=True)),
+ ('is_active', models.BooleanField(default=True)),
+ ('is_complete_profile', models.BooleanField(default=False)),
+ ('passcode', models.IntegerField(blank=True, default=None, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guardian_profile', to=settings.AUTH_USER_MODEL, verbose_name='Email')),
+ ],
+ options={
+ 'verbose_name': 'Guardian',
+ 'db_table': 'guardians',
+ },
+ ),
+ ]
diff --git a/guardian/migrations/0002_remove_guardian_junior_code.py b/guardian/migrations/0002_remove_guardian_junior_code.py
new file mode 100644
index 0000000..6996d0a
--- /dev/null
+++ b/guardian/migrations/0002_remove_guardian_junior_code.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.2 on 2023-06-27 06:15
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='guardian',
+ name='junior_code',
+ ),
+ ]
diff --git a/guardian/migrations/0003_guardian_country_name.py b/guardian/migrations/0003_guardian_country_name.py
new file mode 100644
index 0000000..ea9858b
--- /dev/null
+++ b/guardian/migrations/0003_guardian_country_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-27 13:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0002_remove_guardian_junior_code'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='guardian',
+ name='country_name',
+ field=models.CharField(blank=True, default=None, max_length=30, null=True),
+ ),
+ ]
diff --git a/guardian/migrations/0004_guardian_image.py b/guardian/migrations/0004_guardian_image.py
new file mode 100644
index 0000000..519ed02
--- /dev/null
+++ b/guardian/migrations/0004_guardian_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-28 06:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0003_guardian_country_name'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='guardian',
+ name='image',
+ field=models.ImageField(blank=True, default=None, null=True, upload_to='images/'),
+ ),
+ ]
diff --git a/guardian/migrations/0005_alter_guardian_image.py b/guardian/migrations/0005_alter_guardian_image.py
new file mode 100644
index 0000000..5899e64
--- /dev/null
+++ b/guardian/migrations/0005_alter_guardian_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-29 06:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0004_guardian_image'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='guardian',
+ name='image',
+ field=models.ImageField(blank=True, default=None, null=True, upload_to=''),
+ ),
+ ]
diff --git a/guardian/migrations/0006_guardian_is_verified.py b/guardian/migrations/0006_guardian_is_verified.py
new file mode 100644
index 0000000..5d4082e
--- /dev/null
+++ b/guardian/migrations/0006_guardian_is_verified.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-29 12:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0005_alter_guardian_image'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='guardian',
+ name='is_verified',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/guardian/migrations/0007_alter_guardian_country_name.py b/guardian/migrations/0007_alter_guardian_country_name.py
new file mode 100644
index 0000000..2cbfd73
--- /dev/null
+++ b/guardian/migrations/0007_alter_guardian_country_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-30 10:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0006_guardian_is_verified'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='guardian',
+ name='country_name',
+ field=models.CharField(blank=True, default=None, max_length=100, null=True),
+ ),
+ ]
diff --git a/guardian/migrations/0008_juniortask.py b/guardian/migrations/0008_juniortask.py
new file mode 100644
index 0000000..44a10d7
--- /dev/null
+++ b/guardian/migrations/0008_juniortask.py
@@ -0,0 +1,36 @@
+# Generated by Django 4.2.2 on 2023-07-04 09:24
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0006_alter_junior_country_name'),
+ ('guardian', '0007_alter_guardian_country_name'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='JuniorTask',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('task_name', models.CharField(max_length=100)),
+ ('task_description', models.CharField(max_length=500)),
+ ('points', models.IntegerField(default=5)),
+ ('due_date', models.DateField(blank=True, null=True)),
+ ('image', models.ImageField(blank=True, default=None, null=True, upload_to='')),
+ ('task_status', models.CharField(blank=True, choices=[('1', 'pending'), ('2', 'in-progress'), ('3', 'rejected'), ('4', 'requested'), ('5', 'completed')], default='pending', max_length=15, null=True)),
+ ('is_active', models.BooleanField(default=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('guardian', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guardian', to='guardian.guardian', verbose_name='Guardian')),
+ ('junior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='junior', to='junior.junior', verbose_name='Junior')),
+ ],
+ options={
+ 'verbose_name': 'Junior Task',
+ 'db_table': 'junior_task',
+ },
+ ),
+ ]
diff --git a/guardian/migrations/0009_alter_juniortask_image.py b/guardian/migrations/0009_alter_juniortask_image.py
new file mode 100644
index 0000000..496fee1
--- /dev/null
+++ b/guardian/migrations/0009_alter_juniortask_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-04 12:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0008_juniortask'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='juniortask',
+ name='image',
+ field=models.URLField(blank=True, default=None, null=True),
+ ),
+ ]
diff --git a/guardian/migrations/0010_alter_juniortask_task_status.py b/guardian/migrations/0010_alter_juniortask_task_status.py
new file mode 100644
index 0000000..da84f2c
--- /dev/null
+++ b/guardian/migrations/0010_alter_juniortask_task_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-04 13:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0009_alter_juniortask_image'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='juniortask',
+ name='task_status',
+ field=models.CharField(choices=[('1', 'pending'), ('2', 'in-progress'), ('3', 'rejected'), ('4', 'requested'), ('5', 'completed')], default='pending', max_length=15),
+ ),
+ ]
diff --git a/guardian/migrations/0011_juniortask_default_image_juniortask_is_approved_and_more.py b/guardian/migrations/0011_juniortask_default_image_juniortask_is_approved_and_more.py
new file mode 100644
index 0000000..7d10d3c
--- /dev/null
+++ b/guardian/migrations/0011_juniortask_default_image_juniortask_is_approved_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.2 on 2023-07-05 11:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0010_alter_juniortask_task_status'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='juniortask',
+ name='default_image',
+ field=models.ImageField(blank=True, default=None, null=True, upload_to=''),
+ ),
+ migrations.AddField(
+ model_name='juniortask',
+ name='is_approved',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='juniortask',
+ name='task_status',
+ field=models.CharField(choices=[('1', 'pending'), ('2', 'in-progress'), ('3', 'rejected'), ('4', 'requested'), ('5', 'completed')], default=1, max_length=15),
+ ),
+ ]
diff --git a/guardian/migrations/0012_alter_juniortask_default_image.py b/guardian/migrations/0012_alter_juniortask_default_image.py
new file mode 100644
index 0000000..eefae1b
--- /dev/null
+++ b/guardian/migrations/0012_alter_juniortask_default_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-06 05:12
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0011_juniortask_default_image_juniortask_is_approved_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='juniortask',
+ name='default_image',
+ field=models.URLField(blank=True, default=None, null=True),
+ ),
+ ]
diff --git a/guardian/migrations/0013_alter_guardian_image.py b/guardian/migrations/0013_alter_guardian_image.py
new file mode 100644
index 0000000..0a16630
--- /dev/null
+++ b/guardian/migrations/0013_alter_guardian_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-06 06:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0012_alter_juniortask_default_image'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='guardian',
+ name='image',
+ field=models.URLField(blank=True, default=None, null=True),
+ ),
+ ]
diff --git a/guardian/migrations/0014_guardian_signup_method.py b/guardian/migrations/0014_guardian_signup_method.py
new file mode 100644
index 0000000..c0c1fe7
--- /dev/null
+++ b/guardian/migrations/0014_guardian_signup_method.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-11 11:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0013_alter_guardian_image'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='guardian',
+ name='signup_method',
+ field=models.CharField(choices=[('1', 'manual'), ('2', 'google'), ('3', 'apple')], default='1', max_length=31),
+ ),
+ ]
diff --git a/guardian/migrations/0015_alter_guardian_options_alter_juniortask_options.py b/guardian/migrations/0015_alter_guardian_options_alter_juniortask_options.py
new file mode 100644
index 0000000..45edd24
--- /dev/null
+++ b/guardian/migrations/0015_alter_guardian_options_alter_juniortask_options.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.2 on 2023-07-14 09:34
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0014_guardian_signup_method'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='guardian',
+ options={'verbose_name': 'Guardian', 'verbose_name_plural': 'Guardian'},
+ ),
+ migrations.AlterModelOptions(
+ name='juniortask',
+ options={'verbose_name': 'Junior Task', 'verbose_name_plural': 'Junior Task'},
+ ),
+ ]
diff --git a/guardian/migrations/0016_juniortask_completed_on_juniortask_rejected_on_and_more.py b/guardian/migrations/0016_juniortask_completed_on_juniortask_rejected_on_and_more.py
new file mode 100644
index 0000000..2223e0e
--- /dev/null
+++ b/guardian/migrations/0016_juniortask_completed_on_juniortask_rejected_on_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.2 on 2023-07-18 07:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0015_alter_guardian_options_alter_juniortask_options'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='juniortask',
+ name='completed_on',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='juniortask',
+ name='rejected_on',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='juniortask',
+ name='requested_on',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ ]
diff --git a/guardian/migrations/0017_juniortask_is_invited_juniortask_is_password_set.py b/guardian/migrations/0017_juniortask_is_invited_juniortask_is_password_set.py
new file mode 100644
index 0000000..e340016
--- /dev/null
+++ b/guardian/migrations/0017_juniortask_is_invited_juniortask_is_password_set.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.2 on 2023-07-24 13:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0016_juniortask_completed_on_juniortask_rejected_on_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='juniortask',
+ name='is_invited',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='juniortask',
+ name='is_password_set',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/guardian/migrations/0018_remove_juniortask_is_invited_and_more.py b/guardian/migrations/0018_remove_juniortask_is_invited_and_more.py
new file mode 100644
index 0000000..a9870e1
--- /dev/null
+++ b/guardian/migrations/0018_remove_juniortask_is_invited_and_more.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.2 on 2023-07-24 13:23
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0017_juniortask_is_invited_juniortask_is_password_set'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='juniortask',
+ name='is_invited',
+ ),
+ migrations.RemoveField(
+ model_name='juniortask',
+ name='is_password_set',
+ ),
+ ]
diff --git a/guardian/migrations/0019_guardian_is_invited_guardian_is_password_set.py b/guardian/migrations/0019_guardian_is_invited_guardian_is_password_set.py
new file mode 100644
index 0000000..6c188e4
--- /dev/null
+++ b/guardian/migrations/0019_guardian_is_invited_guardian_is_password_set.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.2 on 2023-07-24 13:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0018_remove_juniortask_is_invited_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='guardian',
+ name='is_invited',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='guardian',
+ name='is_password_set',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/guardian/migrations/0020_alter_juniortask_task_status.py b/guardian/migrations/0020_alter_juniortask_task_status.py
new file mode 100644
index 0000000..241ae10
--- /dev/null
+++ b/guardian/migrations/0020_alter_juniortask_task_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-25 07:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0019_guardian_is_invited_guardian_is_password_set'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='juniortask',
+ name='task_status',
+ field=models.CharField(choices=[('1', 'pending'), ('2', 'in-progress'), ('3', 'rejected'), ('4', 'requested'), ('5', 'completed'), ('6', 'expired')], default=1, max_length=15),
+ ),
+ ]
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/migrations/0022_alter_juniortask_task_description.py b/guardian/migrations/0022_alter_juniortask_task_description.py
new file mode 100644
index 0000000..27e003c
--- /dev/null
+++ b/guardian/migrations/0022_alter_juniortask_task_description.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-09-08 10:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0021_guardian_is_deleted'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='juniortask',
+ name='task_description',
+ field=models.CharField(blank=True, max_length=500, null=True),
+ ),
+ ]
diff --git a/guardian/migrations/__init__.py b/guardian/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/guardian/models.py b/guardian/models.py
new file mode 100644
index 0000000..94cb4e6
--- /dev/null
+++ b/guardian/models.py
@@ -0,0 +1,138 @@
+"""Guardian model file"""
+"""Third party Django app"""
+from django.db import models
+from django.contrib.auth import get_user_model
+"""Import Django app"""
+from base.constants import GENDERS, TASK_STATUS, PENDING, TASK_POINTS, SIGNUP_METHODS
+"""import Junior model"""
+import junior.models
+"""Add user model"""
+User = get_user_model()
+
+# Create your models here.
+# Define junior model with
+# various fields like
+# phone, country code,
+# country name,
+# gender,
+# date of birth,
+# profile image,
+# signup method,
+# guardian code,
+# referral code,
+# referral code that used by the guardian
+# is invited guardian
+# profile is active or not
+# profile is complete or not
+# passcode
+# guardian is verified or not
+"""Define junior task model"""
+# define name of the Task
+# task description
+# points of the task
+# default image of the task
+# image uploaded by junior
+# task status
+# task approved or not
+
+# Create your models here.
+
+class Guardian(models.Model):
+ """Guardian model"""
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='guardian_profile', verbose_name='Email')
+ """Contact details"""
+ country_code = models.IntegerField(blank=True, null=True)
+ phone = models.CharField(max_length=31, null=True, blank=True, default=None)
+ """country name of the guardian"""
+ country_name = models.CharField(max_length=100, null=True, blank=True, default=None)
+ """Image info"""
+ image = models.URLField(null=True, blank=True, default=None)
+ """Personal info"""
+ family_name = models.CharField(max_length=50, null=True, blank=True, default=None)
+ """gender of the guardian"""
+ gender = models.CharField(choices=GENDERS, max_length=15, null=True, blank=True, default=None)
+ """date of birth of the guardian"""
+ dob = models.DateField(max_length=15, null=True, blank=True, default=None)
+ # invited junior"""
+ 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"""
+ is_verified = models.BooleanField(default=False)
+ """guardian profile is complete or not"""
+ is_complete_profile = models.BooleanField(default=False)
+ """passcode of the guardian profile"""
+ passcode = models.IntegerField(null=True, blank=True, default=None)
+ """Sign up method"""
+ signup_method = models.CharField(max_length=31, choices=SIGNUP_METHODS, default='1')
+ """Guardian Codes"""
+ guardian_code = models.CharField(max_length=10, null=True, blank=True, default=None)
+ """Referral code"""
+ referral_code = models.CharField(max_length=10, null=True, blank=True, default=None)
+ """Referral code that is used by guardian while signup"""
+ referral_code_used = models.CharField(max_length=10, null=True, blank=True, default=None)
+ """Profile created and updated time"""
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(object):
+ """ Meta class """
+ db_table = 'guardians'
+ """verbose name of the model"""
+ verbose_name = 'Guardian'
+ """change another name"""
+ verbose_name_plural = 'Guardian'
+
+ def __str__(self):
+ """Return email id"""
+ return f'{self.user}'
+
+class JuniorTask(models.Model):
+ """Junior Task details model"""
+ guardian = models.ForeignKey(Guardian, on_delete=models.CASCADE, related_name='guardian', verbose_name='Guardian')
+ """task details"""
+ task_name = models.CharField(max_length=100)
+ """task description"""
+ task_description = models.CharField(max_length=500, null=True, blank=True)
+ """points of the task"""
+ points = models.IntegerField(default=TASK_POINTS)
+ """last date of the task"""
+ due_date = models.DateField(auto_now_add=False, null=True, blank=True)
+ """Images of task that is upload by guardian"""
+ default_image = models.URLField(null=True, blank=True, default=None)
+ """image that is uploaded by junior"""
+ image = models.URLField(null=True, blank=True, default=None)
+ """associated junior with the task"""
+ junior = models.ForeignKey('junior.Junior', on_delete=models.CASCADE, related_name='junior', verbose_name='Junior')
+ """task status"""
+ task_status = models.CharField(choices=TASK_STATUS, max_length=15, default=PENDING)
+ """task is active or not"""
+ is_active = models.BooleanField(default=True)
+ """Task is approved or not"""
+ is_approved = models.BooleanField(default=False)
+ """request task on particular date"""
+ requested_on = models.DateTimeField(auto_now_add=False, null=True, blank=True)
+ """reject task on particular date"""
+ rejected_on = models.DateTimeField(auto_now_add=False, null=True, blank=True)
+ """complete task on particular date"""
+ completed_on = models.DateTimeField(auto_now_add=False, null=True, blank=True)
+ """Profile created and updated time"""
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(object):
+ """ Meta class """
+ db_table = 'junior_task'
+ """verbose name of the model"""
+ verbose_name = 'Junior Task'
+ verbose_name_plural = 'Junior Task'
+
+ def __str__(self):
+ """Return email id"""
+ return f'{self.task_name}'
+
+
diff --git a/guardian/serializers.py b/guardian/serializers.py
new file mode 100644
index 0000000..a49286b
--- /dev/null
+++ b/guardian/serializers.py
@@ -0,0 +1,529 @@
+"""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
+from django.db import transaction
+from django.contrib.auth.models import User
+from datetime import datetime, time
+import pytz
+from django.utils import timezone
+# Import guardian's model,
+# Import junior's model,
+# Import account's model,
+# Import constant from
+# base package,
+# Import messages from
+# base package,
+# Import some functions
+# local imports
+from .models import Guardian, JuniorTask
+from account.models import UserProfile, UserEmailOtp, UserNotification
+from account.utils import generate_code
+from junior.serializers import JuniorDetailSerializer
+from base.messages import ERROR_CODE, SUCCESS_CODE
+from base.constants import NUMBER, JUN, ZOD, GRD, Already_register_user, GUARDIAN
+from junior.models import Junior, JuniorPoints, JuniorGuardianRelationship
+from .utils import real_time, convert_timedelta_into_datetime, update_referral_points
+# notification's constant
+from notifications.constants import TASK_APPROVED, TASK_REJECTED, TASK_ASSIGNED
+# send notification function
+from notifications.utils import send_notification
+from django.core.exceptions import ValidationError
+from django.utils.translation import gettext as _
+
+# In this serializer file
+# define user serializer,
+# define password validation
+# create guardian serializer,
+# task serializer,
+# guardian serializer,
+# task details serializer,
+# top junior serializer,
+# guardian profile serializer,
+# approve junior serializer,
+# approve task serializer,
+from rest_framework import serializers
+
+class PasswordValidator:
+ """Password validation"""
+ def __init__(self, min_length=8, max_length=None, require_uppercase=True, require_numbers=True):
+ self.min_length = min_length
+ self.max_length = max_length
+ self.require_uppercase = require_uppercase
+ self.require_numbers = require_numbers
+
+ def __call__(self, value):
+ self.enforce_password_policy(value)
+
+ def enforce_password_policy(self, password):
+ # add validation for password
+ special_characters = "!@#$%^&*()_-+=<>?/[]{}|"
+ if len(password) < self.min_length:
+ raise serializers.ValidationError(
+ _("Password must be at least %(min_length)d characters long.") % {'min_length': self.min_length}
+ )
+
+ if self.max_length is not None and len(password) > self.max_length:
+ # must be 8 character
+ raise serializers.ValidationError(
+ _("Password must be at most %(max_length)d characters long.") % {'max_length': self.max_length}
+ )
+
+ if self.require_uppercase and not any(char.isupper() for char in password):
+ # must contain upper case letter
+ raise serializers.ValidationError(_("Password must contain at least one uppercase letter."))
+
+ if self.require_numbers and not any(char.isdigit() for char in password):
+ # must contain digit
+ raise serializers.ValidationError(_("Password must contain at least one digit."))
+ if self.require_numbers and not any(char in special_characters for char in password):
+ # must contain special character
+ raise serializers.ValidationError(_("Password must contain at least one special character."))
+
+
+
+
+class UserSerializer(serializers.ModelSerializer):
+ """User serializer"""
+ auth_token = serializers.SerializerMethodField('get_auth_token')
+ password = serializers.CharField(write_only=True, validators=[PasswordValidator()])
+
+ class Meta(object):
+ """Meta info"""
+ model = User
+ fields = ['id', 'email', 'password', 'auth_token']
+
+ def get_auth_token(self, obj):
+ """generate auth token"""
+ refresh = RefreshToken.for_user(obj)
+ access_token = str(refresh.access_token)
+ return access_token
+ def create(self, validated_data):
+ """fetch data"""
+ email = validated_data.get('email')
+ user_type = self.context
+ password = validated_data.get('password')
+ try:
+ """Create user profile"""
+ user = User.objects.create_user(username=email, email=email, password=password)
+ UserNotification.objects.get_or_create(user=user)
+ if user_type == str(NUMBER['one']):
+ # create junior profile
+ junior = Junior.objects.create(auth=user, junior_code=generate_code(JUN, user.id),
+ referral_code=generate_code(ZOD, user.id))
+ position = Junior.objects.all().count()
+ JuniorPoints.objects.create(junior=junior, position=position)
+ if user_type == str(NUMBER['two']):
+ # create guardian profile
+ Guardian.objects.create(user=user, guardian_code=generate_code(GRD, user.id),
+ referral_code=generate_code(ZOD, user.id))
+ return user
+ except Exception as e:
+ """Error handling"""
+ logging.error(e)
+ otp = UserEmailOtp.objects.filter(email=email).last()
+ otp_verified = False
+ if otp and otp.is_verified:
+ otp_verified = True
+ raise serializers.ValidationError({"details": ERROR_CODE['2021'], "otp_verified":bool(otp_verified),
+ "code": 400, "status":"failed",
+ })
+
+# update guardian profile
+class CreateGuardianSerializer(serializers.ModelSerializer):
+ """Create guardian serializer"""
+ """Basic info"""
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+ email = serializers.SerializerMethodField('get_email')
+ """Contact details"""
+ phone = serializers.CharField(max_length=20, required=False)
+ country_code = serializers.IntegerField(required=False)
+ # basic info
+ family_name = serializers.CharField(max_length=100, required=False)
+ dob = serializers.DateField(required=False)
+ # code info
+ referral_code = serializers.CharField(max_length=100, required=False)
+ # image info
+ image = serializers.URLField(required=False)
+
+ class Meta(object):
+ """Meta info"""
+ model = Guardian
+ fields = ['id', 'first_name', 'last_name', 'email', 'phone', 'family_name', 'gender', 'country_code',
+ 'dob', 'referral_code', 'passcode', 'is_complete_profile', 'referral_code_used',
+ 'country_name', 'image']
+
+ def get_first_name(self,obj):
+ """first name of guardian"""
+ return obj.user.first_name
+
+ def get_last_name(self,obj):
+ """last name of guardian"""
+ return obj.user.last_name
+
+ def get_email(self,obj):
+ """emailof guardian"""
+ return obj.user.email
+
+ def create(self, validated_data):
+ """Create guardian profile"""
+ user = User.objects.filter(username=self.context['user']).last()
+ if user:
+ """Save first and last name of guardian"""
+ if self.context.get('first_name') != '' and self.context.get('first_name') is not None:
+ user.first_name = self.context.get('first_name')
+ if self.context.get('last_name') != '' and self.context.get('last_name') is not None:
+ user.last_name = self.context.get('last_name')
+ user.save()
+ """Create guardian data"""
+ guardian = Guardian.objects.filter(user=self.context['user']).last()
+ if guardian:
+ """update details according to the data get from request"""
+ guardian.gender = validated_data.get('gender',guardian.gender)
+ guardian.family_name = validated_data.get('family_name', guardian.family_name)
+ guardian.dob = validated_data.get('dob',guardian.dob)
+ """Update country code and phone number"""
+ guardian.phone = validated_data.get('phone', guardian.phone)
+ guardian.country_code = validated_data.get('country_code', guardian.country_code)
+ guardian.passcode = validated_data.get('passcode', guardian.passcode)
+ guardian.country_name = validated_data.get('country_name', guardian.country_name)
+ guardian.image = validated_data.get('image', guardian.image)
+ """Complete profile of the junior if below all data are filled"""
+ complete_profile_field = all([guardian.gender, guardian.country_name,
+ guardian.dob, user.first_name,
+ guardian.image])
+ guardian.is_complete_profile = False
+ if complete_profile_field:
+ guardian.is_complete_profile = True
+ referral_code_used = validated_data.get('referral_code_used')
+ update_referral_points(guardian.referral_code, referral_code_used)
+ guardian.referral_code_used = validated_data.get('referral_code_used', guardian.referral_code_used)
+ guardian.save()
+ return guardian
+
+ def save(self, **kwargs):
+ """Save the data into junior table"""
+ with transaction.atomic():
+ instance = super().save(**kwargs)
+ return instance
+
+
+
+class TaskSerializer(serializers.ModelSerializer):
+ """Task serializer"""
+ class Meta(object):
+ """Meta info"""
+ model = JuniorTask
+ fields = ['id', 'task_name','task_description','points', 'due_date','default_image']
+
+ def validate_due_date(self, value):
+ """validation on due date"""
+ if value < datetime.today().date():
+ raise serializers.ValidationError({"details": ERROR_CODE['2046'],
+ "code": 400, "status": "failed",
+ })
+ return value
+ def create(self, validated_data):
+ """create default task image data"""
+ guardian = self.context['guardian']
+ # update image of the task
+ images = self.context['image']
+ junior_data = self.context['junior_data']
+ tasks_created = []
+
+ for junior in junior_data:
+ # create task
+ task_data = validated_data.copy()
+ task_data['guardian'] = guardian
+ task_data['default_image'] = images
+ task_data['junior'] = junior
+ instance = JuniorTask.objects.create(**task_data)
+ tasks_created.append(instance)
+ send_notification.delay(TASK_ASSIGNED, guardian.user_id, GUARDIAN,
+ junior.auth_id, {'task_id': instance.id})
+ return instance
+
+class GuardianDetailSerializer(serializers.ModelSerializer):
+ """junior serializer"""
+
+ email = serializers.SerializerMethodField('get_auth')
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+
+ def get_auth(self, obj):
+ """user email address"""
+ return obj.user.username
+
+ def get_first_name(self, obj):
+ """user first name"""
+ return obj.user.first_name
+
+ def get_last_name(self, obj):
+ """user last name"""
+ return obj.user.last_name
+ class Meta(object):
+ """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', 'is_deleted',
+ 'updated_at']
+class TaskDetailsSerializer(serializers.ModelSerializer):
+ """Task detail serializer"""
+
+ junior = JuniorDetailSerializer()
+ remaining_time = serializers.SerializerMethodField('get_remaining_time')
+ is_expired = serializers.SerializerMethodField('get_is_expired')
+
+ def get_remaining_time(self, obj):
+ """ remaining time to complete task"""
+ due_date_datetime = datetime.combine(obj.due_date, datetime.max.time())
+ # fetch real time
+ # current_datetime = real_time()
+ # new code
+ due_date_datetime = due_date_datetime.astimezone(pytz.utc)
+ current_datetime = timezone.now().astimezone(pytz.utc)
+ # Perform the subtraction
+ if due_date_datetime > current_datetime:
+ time_difference = due_date_datetime - current_datetime
+ time_only = convert_timedelta_into_datetime(time_difference)
+ return str(time_difference.days) + ' days ' + str(time_only)
+ return str(NUMBER['zero']) + ' days ' + '00:00:00:00000'
+
+ def get_is_expired(self, obj):
+ """ task expired or not"""
+ if obj.due_date < datetime.today().date():
+ return True
+ return False
+ class Meta(object):
+ """Meta info"""
+ model = JuniorTask
+ fields = ['id', 'guardian', 'task_name', 'task_description', 'points', 'due_date','default_image', 'image',
+ 'requested_on', 'rejected_on', 'completed_on', 'is_expired',
+ 'junior', 'task_status', 'is_active', 'remaining_time', 'created_at','updated_at']
+
+class TaskDetailsjuniorSerializer(serializers.ModelSerializer):
+ """Task detail serializer"""
+
+ guardian = GuardianDetailSerializer()
+ remaining_time = serializers.SerializerMethodField('get_remaining_time')
+
+ def get_remaining_time(self, obj):
+ """ remaining time to complete task"""
+ due_date_datetime = datetime.combine(obj.due_date, datetime.max.time())
+ # fetch real time
+ # current_datetime = real_time()
+ # new code
+ due_date_datetime = due_date_datetime.astimezone(pytz.utc)
+ current_datetime = timezone.now().astimezone(pytz.utc)
+ # Perform the subtraction
+ if due_date_datetime > current_datetime:
+ time_difference = due_date_datetime - current_datetime
+ time_only = convert_timedelta_into_datetime(time_difference)
+ return str(time_difference.days) + ' days ' + str(time_only)
+ return str(NUMBER['zero']) + ' days ' + '00:00:00:00000'
+
+
+ class Meta(object):
+ """Meta info"""
+ model = JuniorTask
+ fields = ['id', 'guardian', 'task_name', 'task_description', 'points', 'due_date','default_image', 'image',
+ 'requested_on', 'rejected_on', 'completed_on',
+ 'junior', 'task_status', 'is_active', 'remaining_time', 'created_at','updated_at']
+
+
+class TopJuniorSerializer(serializers.ModelSerializer):
+ """Top junior serializer"""
+ junior = JuniorDetailSerializer()
+ position = serializers.SerializerMethodField()
+
+ class Meta(object):
+ """Meta info"""
+ model = JuniorPoints
+ fields = ['id', 'junior', 'total_points', 'position', 'created_at', 'updated_at']
+
+ def to_representation(self, instance):
+ """Convert instance to representation"""
+ representation = super().to_representation(instance)
+ return representation
+
+ @staticmethod
+ def get_position(obj):
+ """ get position/rank """
+ return obj.rank
+
+
+class GuardianProfileSerializer(serializers.ModelSerializer):
+ """junior serializer"""
+
+ email = serializers.SerializerMethodField('get_auth')
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+ total_count = serializers.SerializerMethodField('get_total_count')
+ complete_field_count = serializers.SerializerMethodField('get_complete_field_count')
+ notification_count = serializers.SerializerMethodField('get_notification_count')
+
+ def get_auth(self, obj):
+ """user email address"""
+ return obj.user.username
+
+ def get_first_name(self, obj):
+ """user first name"""
+ return obj.user.first_name
+
+ def get_last_name(self, obj):
+ """user last name"""
+ return obj.user.last_name
+
+ def get_total_count(self, obj):
+ """total fields count"""
+ return NUMBER['five']
+
+ def get_complete_field_count(self, obj):
+ """total filled fields count"""
+ total_field_list = [obj.user.first_name, obj.country_name,
+ obj.gender, obj.dob, obj.image]
+ # count total complete field
+ total_complete_field = [data for data in total_field_list if data != '' and data is not None]
+ return len(total_complete_field)
+
+ def get_notification_count(self, obj):
+ """total notification count"""
+ return 0
+
+ class Meta(object):
+ """Meta info"""
+ model = Guardian
+ 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','is_deleted']
+
+
+class ApproveJuniorSerializer(serializers.ModelSerializer):
+ """approve junior serializer"""
+ class Meta(object):
+ """Meta info"""
+ model = Junior
+ fields = ['id', 'guardian_code']
+
+ def create(self, validated_data):
+ """update guardian code"""
+ instance = self.context['junior']
+ guardian_code = self.context['guardian_code']
+ index = instance.guardian_code.index(guardian_code)
+ instance.guardian_code_status[index] = str(NUMBER['two'])
+ instance.save()
+ return instance
+
+
+class ApproveTaskSerializer(serializers.ModelSerializer):
+ """approve task serializer"""
+ class Meta(object):
+ """Meta info"""
+ model = JuniorTask
+ fields = ['id', 'task_status', 'is_approved']
+
+ def create(self, validated_data):
+ """update task status """
+ with transaction.atomic():
+ instance = self.context['task_instance']
+ junior = self.context['junior']
+ junior_details = Junior.objects.filter(id=junior).last()
+ junior_data, created = JuniorPoints.objects.get_or_create(junior=junior_details)
+ if self.context['action'] == str(NUMBER['one']):
+ # approve the task
+ instance.task_status = str(NUMBER['five'])
+ instance.is_approved = True
+ # update total task point
+ junior_data.total_points = junior_data.total_points + instance.points
+ # update complete time of task
+ # instance.completed_on = real_time()
+ instance.completed_on = timezone.now().astimezone(pytz.utc)
+ send_notification.delay(TASK_APPROVED, instance.guardian.user_id, GUARDIAN,
+ junior_details.auth_id, {'task_id': instance.id})
+ else:
+ # reject the task
+ instance.task_status = str(NUMBER['three'])
+ instance.is_approved = False
+ # update reject time of task
+ # instance.rejected_on = real_time()
+ instance.rejected_on = timezone.now().astimezone(pytz.utc)
+ send_notification.delay(TASK_REJECTED, instance.guardian.user_id, GUARDIAN,
+ junior_details.auth_id, {'task_id': instance.id})
+ instance.save()
+ junior_data.save()
+ return junior_details
+
+class GuardianDetailListSerializer(serializers.ModelSerializer):
+ """Guardian serializer"""
+
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+ email = serializers.SerializerMethodField('get_email')
+ image = serializers.SerializerMethodField('get_image')
+ guardian_id = serializers.SerializerMethodField('get_guardian_id')
+ guardian_code = serializers.SerializerMethodField('get_guardian_code')
+ gender = serializers.SerializerMethodField('get_gender')
+ phone = serializers.SerializerMethodField('get_phone')
+ country_name = serializers.SerializerMethodField('get_country_name')
+ dob = serializers.SerializerMethodField('get_dob')
+ guardian_code_status = serializers.SerializerMethodField('get_guardian_code_status')
+ # code info
+
+
+ class Meta(object):
+ """Meta info"""
+ model = JuniorGuardianRelationship
+ fields = ['guardian_id', 'first_name', 'last_name', 'email', 'relationship', 'image', 'dob',
+ 'guardian_code', 'gender', 'phone', 'country_name', 'created_at', 'guardian_code_status',
+ 'updated_at']
+
+ def get_guardian_id(self,obj):
+ """first name of guardian"""
+ return obj.guardian.id
+ def get_first_name(self,obj):
+ """first name of guardian"""
+ return obj.guardian.user.first_name
+
+ def get_last_name(self,obj):
+ """last name of guardian"""
+ return obj.guardian.user.last_name
+
+ def get_email(self,obj):
+ """email of guardian"""
+ return obj.guardian.user.email
+
+ def get_image(self,obj):
+ """guardian image"""
+ return obj.guardian.image
+
+ def get_guardian_code(self,obj):
+ """ guardian code"""
+ return obj.guardian.guardian_code
+
+ def get_gender(self,obj):
+ """ guardian gender"""
+ return obj.guardian.gender
+
+ def get_phone(self,obj):
+ """guardian phone"""
+ return obj.guardian.phone
+
+ def get_country_name(self,obj):
+ """ guardian country name """
+ return obj.guardian.country_name
+
+ def get_dob(self,obj):
+ """guardian dob """
+ return obj.guardian.dob
+
+ def get_guardian_code_status(self,obj):
+ """guardian code status"""
+ if obj.guardian.guardian_code in obj.junior.guardian_code:
+ index = obj.junior.guardian_code.index(obj.guardian.guardian_code)
+ data = obj.junior.guardian_code_status[index]
+ return data
diff --git a/guardian/tasks.py b/guardian/tasks.py
new file mode 100644
index 0000000..f40d232
--- /dev/null
+++ b/guardian/tasks.py
@@ -0,0 +1,12 @@
+"""task files"""
+
+# Django import
+import secrets
+
+
+def generate_otp():
+ """
+ generate random otp
+ """
+ digits = "0123456789"
+ return "".join(secrets.choice(digits) for _ in range(6))
diff --git a/guardian/tests.py b/guardian/tests.py
new file mode 100644
index 0000000..3036e8b
--- /dev/null
+++ b/guardian/tests.py
@@ -0,0 +1,5 @@
+"""Test file of Guardian"""
+"""Third party Django app"""
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/guardian/urls.py b/guardian/urls.py
new file mode 100644
index 0000000..4a1d006
--- /dev/null
+++ b/guardian/urls.py
@@ -0,0 +1,43 @@
+""" Urls files"""
+"""Django import"""
+from django.urls import path, include
+from .views import (SignupViewset, UpdateGuardianProfile, CreateTaskAPIView, TaskListAPIView,
+ SearchTaskListAPIView, TopJuniorListAPIView, ApproveJuniorAPIView, ApproveTaskAPIView,
+ GuardianListAPIView)
+"""Third party import"""
+from rest_framework import routers
+
+"""Define Router"""
+router = routers.SimpleRouter()
+
+# API End points with router
+# in this file
+# we define various api end point
+# that is covered in this guardian
+# section API:- like
+# sign-up, create guardian profile,
+# create-task,
+# all task list, top junior,
+# filter-task"""
+"""Sign up API"""
+router.register('sign-up', SignupViewset, basename='sign-up')
+# Create guardian profile API"""
+router.register('create-guardian-profile', UpdateGuardianProfile, basename='update-guardian-profile')
+# Create Task API"""
+router.register('create-task', CreateTaskAPIView, basename='create-task')
+# Task list bases on the status API"""
+router.register('task-list', TaskListAPIView, basename='task-list')
+# Leaderboard API"""
+router.register('top-junior', TopJuniorListAPIView, basename='top-junior')
+# Search Task list on the bases of status, due date, and task title API"""
+router.register('filter-task', SearchTaskListAPIView, basename='filter-task')
+# Approve junior API"""
+router.register('approve-junior', ApproveJuniorAPIView, basename='approve-junior')
+# Approve junior API"""
+router.register('approve-task', ApproveTaskAPIView, basename='approve-task')
+# guardian list API"""
+router.register('guardian-list', GuardianListAPIView, basename='guardian-list')
+# Define Url pattern"""
+urlpatterns = [
+ path('api/v1/', include(router.urls)),
+]
diff --git a/guardian/utils.py b/guardian/utils.py
new file mode 100644
index 0000000..6461912
--- /dev/null
+++ b/guardian/utils.py
@@ -0,0 +1,134 @@
+"""Utiles file of guardian"""
+"""Django import"""
+import oss2
+"""Import setting"""
+from django.conf import settings
+import logging
+import requests
+from django.core.exceptions import ObjectDoesNotExist
+"""Import tempfile"""
+import tempfile
+# Import date time module's function
+from datetime import datetime, time
+# import Number constant
+from base.constants import NUMBER, GUARDIAN
+# Import Junior's model
+from junior.models import Junior, JuniorPoints
+# Import guardian's model
+from .models import JuniorTask
+# Import app from celery
+from zod_bank.celery import app
+# notification's constant
+from notifications.constants import REFERRAL_POINTS
+# send notification function
+from notifications.utils import send_notification
+
+
+# Define upload image on
+# ali baba cloud
+# firstly save image
+# in temporary file
+# then check bucket name
+# then upload on ali baba
+# bucket and reform the image url"""
+# fetch real time data without depend on system time
+# convert time delta into date time object
+# update junior total points
+# update referral points
+# if any one use referral code of the junior
+# junior earn 5 points
+
+def upload_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.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}"
+
+
+def real_time():
+ """fetch real time from world time api"""
+ url = time_url
+ response = requests.get(url)
+ if response.status_code == 200:
+ data = response.json()
+ time_str = data['datetime']
+ realtime = datetime.fromisoformat(time_str.replace('Z', '+00:00')).replace(tzinfo=None)
+ return realtime
+ else:
+ logging.error("Could not fetch error")
+ return None
+
+
+def convert_timedelta_into_datetime(time_difference):
+ """convert date time"""
+ # convert timedelta into datetime format
+ hours = time_difference.seconds // NUMBER['thirty_six_hundred']
+ minutes = (time_difference.seconds // NUMBER['sixty']) % NUMBER['sixty']
+ seconds = time_difference.seconds % NUMBER['sixty']
+ microseconds = time_difference.microseconds
+ # Create a new time object with the extracted time components
+ time_only = time(hours, minutes, seconds, microseconds)
+ return time_only
+
+def update_referral_points(referral_code, referral_code_used):
+ """Update benefit of referral points"""
+ junior_queryset = Junior.objects.filter(referral_code=referral_code_used).last()
+ if junior_queryset and junior_queryset.referral_code != referral_code:
+ # create data if junior points is not exist
+ junior_query, created = JuniorPoints.objects.get_or_create(junior=junior_queryset)
+ if junior_query:
+ # update junior total points and referral points
+ 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, GUARDIAN, junior_queryset.auth_id, {})
+
+
+
+@app.task
+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'])]
+ JuniorTask.objects.filter(due_date__lt=datetime.today().date(),
+ task_status__in=task_status).update(task_status=str(NUMBER['six']))
+ except ObjectDoesNotExist as e:
+ logging.error(str(e))
diff --git a/guardian/views.py b/guardian/views.py
new file mode 100644
index 0000000..5e89417
--- /dev/null
+++ b/guardian/views.py
@@ -0,0 +1,410 @@
+"""Views of Guardian"""
+import math
+
+# django imports
+# Import IsAuthenticated
+# Import viewsets and status
+# Import PageNumberPagination
+# Import User
+# Import timezone
+from django.db.models import F, Window
+from django.db.models.functions.window import Rank
+from rest_framework.permissions import IsAuthenticated
+from rest_framework import viewsets, status
+from rest_framework.pagination import PageNumberPagination
+from django.contrib.auth.models import User
+from base.constants import guardian_code_tuple
+from rest_framework.filters import SearchFilter
+from django.utils import timezone
+
+from base.pagination import CustomPageNumberPagination
+# Import guardian's model,
+# Import junior's model,
+# Import account's model,
+# Import constant from
+# base package,
+# Import messages from
+# base package,
+# Import some functions
+# from utils file
+# Import account's serializer
+# Import account's task
+# Import notification constant
+# Import send_notification function
+from .serializers import (UserSerializer, CreateGuardianSerializer, TaskSerializer, TaskDetailsSerializer,
+ TopJuniorSerializer, ApproveJuniorSerializer, ApproveTaskSerializer,
+ GuardianDetailListSerializer)
+from .models import Guardian, JuniorTask
+from junior.models import Junior, JuniorPoints, JuniorGuardianRelationship
+from account.models import UserEmailOtp, UserNotification, UserDeviceDetails
+from .tasks import generate_otp
+from account.utils import custom_response, custom_error_response, send_otp_email, task_status_fun
+from base.messages import ERROR_CODE, SUCCESS_CODE
+from base.constants import NUMBER, GUARDIAN_CODE_STATUS, GUARDIAN
+from .utils import upload_image_to_alibaba
+from notifications.constants import REGISTRATION, TASK_ASSIGNED, ASSOCIATE_APPROVED, ASSOCIATE_REJECTED
+from notifications.utils import send_notification
+
+""" Define APIs """
+# Define Signup API,
+# update guardian profile,
+# list of all task
+# list of task according to the status of the task
+# create task API
+# search task by name of the task API
+# top junior API,
+# approve junior API
+# approve task API
+# Create your views here.
+# create approve task API
+class SignupViewset(viewsets.ModelViewSet):
+ """Signup view set"""
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
+ http_method_names = ('post',)
+ def create(self, request, *args, **kwargs):
+ """Create user profile"""
+ device_id = request.META.get('HTTP_DEVICE_ID')
+ if request.data['user_type'] in [str(NUMBER['one']), str(NUMBER['two'])]:
+ serializer = UserSerializer(context=request.data['user_type'], data=request.data)
+ if serializer.is_valid():
+ user = serializer.save()
+ """Generate otp"""
+ otp = generate_otp()
+ # expire otp after 1 day
+ expiry = timezone.now() + timezone.timedelta(days=1)
+ # create user email otp object
+ UserEmailOtp.objects.create(email=request.data['email'], otp=otp,
+ user_type=str(request.data['user_type']), expired_at=expiry)
+ """Send email to the register user"""
+ send_otp_email.delay(request.data['email'], otp)
+ UserDeviceDetails.objects.create(user=user, device_id=device_id)
+ return custom_response(SUCCESS_CODE['3001'],
+ response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+ else:
+ return custom_error_response(ERROR_CODE['2028'], response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class UpdateGuardianProfile(viewsets.ModelViewSet):
+ """Update guardian profile"""
+ serializer_class = CreateGuardianSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('post',)
+
+ def create(self, request, *args, **kwargs):
+ """Create guardian profile"""
+ try:
+ data = request.data
+ image = request.data.get('image')
+ image_url = ''
+ if image:
+ if image and image.size == NUMBER['zero']:
+ return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST)
+ filename = f"images/{image.name}"
+ # upload image on ali baba
+ image_url = upload_image_to_alibaba(image, filename)
+ data = {"image":image_url}
+ serializer = CreateGuardianSerializer(context={"user":request.user,
+ "first_name":request.data.get('first_name'),
+ "last_name": request.data.get('last_name'),
+ "image":image_url},
+ data=data)
+ if serializer.is_valid():
+ """save serializer"""
+ serializer.save()
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class AllTaskListAPIView(viewsets.ModelViewSet):
+ """Update guardian profile"""
+ serializer_class = TaskDetailsSerializer
+ permission_classes = [IsAuthenticated]
+
+ def list(self, request, *args, **kwargs):
+ """Create guardian profile"""
+ queryset = JuniorTask.objects.filter(guardian__user=request.user)
+ # use TaskDetailsSerializer serializer
+ serializer = TaskDetailsSerializer(queryset, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+
+
+class TaskListAPIView(viewsets.ModelViewSet):
+ """Task list
+ Params
+ status
+ search
+ page
+ junior"""
+ serializer_class = TaskDetailsSerializer
+ permission_classes = [IsAuthenticated]
+ filter_backends = (SearchFilter,)
+ search_fields = ['task_name', ]
+ pagination_class = PageNumberPagination
+ http_method_names = ('get',)
+
+ def get_queryset(self):
+ queryset = JuniorTask.objects.filter(guardian__user=self.request.user
+ ).select_related('junior', 'junior__auth'
+ ).order_by('-created_at')
+
+ queryset = self.filter_queryset(queryset)
+ return queryset
+
+ def list(self, request, *args, **kwargs):
+ """Create guardian profile"""
+ status_value = self.request.GET.get('status')
+ junior = self.request.GET.get('junior')
+ queryset = self.get_queryset()
+ task_status = task_status_fun(status_value)
+ if status_value:
+ queryset = queryset.filter(task_status__in=task_status)
+ if junior:
+ queryset = queryset.filter(junior=int(junior))
+ paginator = CustomPageNumberPagination()
+ # use Pagination
+ paginated_queryset = paginator.paginate_queryset(queryset, request)
+ # use TaskDetailsSerializer serializer
+ serializer = self.serializer_class(paginated_queryset, many=True)
+ return paginator.get_paginated_response(serializer.data)
+
+
+class CreateTaskAPIView(viewsets.ModelViewSet):
+ """create task for junior"""
+ serializer_class = TaskSerializer
+ queryset = JuniorTask.objects.all()
+ http_method_names = ('post', )
+
+ def create(self, request, *args, **kwargs):
+ """
+ image should be in form data
+ """
+ try:
+ image = request.data['default_image']
+ junior_ids = request.data['junior'].split(',')
+
+ invalid_junior_ids = [junior_id for junior_id in junior_ids if not junior_id.isnumeric()]
+ if invalid_junior_ids:
+ # At least one junior value is not an integer
+ return custom_error_response(ERROR_CODE['2047'], response_status=status.HTTP_400_BAD_REQUEST)
+
+ allowed_extensions = ['.jpg', '.jpeg', '.png']
+ if not any(extension in str(image) for extension in allowed_extensions):
+ return custom_error_response(ERROR_CODE['2048'], response_status=status.HTTP_400_BAD_REQUEST)
+ if 'https' in str(image):
+ image_data = image
+ else:
+ filename = f"images/{image}"
+ if image and image.size == NUMBER['zero']:
+ return custom_error_response(ERROR_CODE['2035'],
+ response_status=status.HTTP_400_BAD_REQUEST)
+ image_data = upload_image_to_alibaba(image, filename)
+ request.data.pop('default_image')
+
+ guardian = Guardian.objects.filter(user=request.user).select_related('user').last()
+ junior_data = Junior.objects.filter(id__in=junior_ids,
+ guardian_code__contains=[guardian.guardian_code]
+ ).select_related('auth')
+ if not junior_data:
+ return custom_error_response(ERROR_CODE['2047'], response_status=status.HTTP_400_BAD_REQUEST)
+ for junior in junior_data:
+ index = junior.guardian_code.index(guardian.guardian_code)
+ status_index = junior.guardian_code_status[index]
+ if status_index == str(NUMBER['three']):
+ return custom_error_response(ERROR_CODE['2078'], response_status=status.HTTP_400_BAD_REQUEST)
+
+ # use TaskSerializer serializer
+ serializer = TaskSerializer(context={"guardian": guardian, "image": image_data,
+ "junior_data": junior_data}, data=request.data)
+ if serializer.is_valid():
+ # save serializer
+ serializer.save()
+ # removed send notification method and used in serializer
+ return custom_response(SUCCESS_CODE['3018'], response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class SearchTaskListAPIView(viewsets.ModelViewSet):
+ """Filter task"""
+ serializer_class = TaskDetailsSerializer
+ permission_classes = [IsAuthenticated]
+ pagination_class = PageNumberPagination
+ http_method_names = ('get',)
+
+ def get_queryset(self):
+ """Get the queryset for the view"""
+ title = self.request.GET.get('title')
+ # fetch junior query
+ junior_queryset = JuniorTask.objects.filter(guardian__user=self.request.user, task_name__icontains=title)\
+ .order_by('due_date', 'created_at')
+ return junior_queryset
+
+ def list(self, request, *args, **kwargs):
+ """Filter task"""
+ try:
+ queryset = self.get_queryset()
+ paginator = self.pagination_class()
+ # use pagination
+ paginated_queryset = paginator.paginate_queryset(queryset, request)
+ # use TaskSerializer serializer
+ serializer = TaskDetailsSerializer(paginated_queryset, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class TopJuniorListAPIView(viewsets.ModelViewSet):
+ """Top juniors list
+ No Params"""
+ serializer_class = TopJuniorSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+
+ def get_serializer_context(self):
+ # context list
+ context = super().get_serializer_context()
+ context.update({'view': self})
+ return context
+
+ def get_queryset(self):
+ queryset = JuniorPoints.objects.filter(
+ junior__is_verified=True
+ ).select_related(
+ 'junior', 'junior__auth'
+ ).annotate(rank=Window(
+ expression=Rank(),
+ order_by=[F('total_points').desc(), 'junior__created_at'])
+ ).order_by('-total_points', 'junior__created_at')
+ return queryset
+
+ def list(self, request, *args, **kwargs):
+ """Fetch junior list of those who complete their tasks"""
+ try:
+ junior_total_points = self.get_queryset()[:15]
+ serializer = self.get_serializer(junior_total_points, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class ApproveJuniorAPIView(viewsets.ModelViewSet):
+ """approve junior by guardian"""
+ serializer_class = ApproveJuniorSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('post',)
+
+ def create(self, request, *args, **kwargs):
+ """ Use below param
+ {"junior_id":"75",
+ "action":"1"}
+ """
+ try:
+ relation_obj = JuniorGuardianRelationship.objects.filter(
+ guardian__user__email=self.request.user,
+ junior__id=self.request.data.get('junior_id')
+ ).select_related('guardian', 'junior').first()
+ guardian = relation_obj.guardian
+ # fetch junior query
+ junior_queryset = relation_obj.junior
+ 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": guardian.guardian_code,
+ "junior": junior_queryset,
+ "action": request.data['action']},
+ data=request.data)
+ if serializer.is_valid():
+ # save serializer
+ serializer.save()
+ send_notification.delay(ASSOCIATE_APPROVED, guardian.user_id, GUARDIAN,
+ junior_queryset.auth_id, {})
+ return custom_response(SUCCESS_CODE['3023'], serializer.data, response_status=status.HTTP_200_OK)
+ else:
+ if junior_queryset.guardian_code and ('-' in junior_queryset.guardian_code):
+ junior_queryset.guardian_code.remove('-')
+ if junior_queryset.guardian_code_status and ('-' in junior_queryset.guardian_code_status):
+ junior_queryset.guardian_code_status.remove('-')
+ index = junior_queryset.guardian_code.index(guardian.guardian_code)
+ junior_queryset.guardian_code.remove(guardian.guardian_code)
+ junior_queryset.guardian_code_status.pop(index)
+ junior_queryset.save()
+ send_notification.delay(ASSOCIATE_REJECTED, guardian.user_id, GUARDIAN, junior_queryset.auth_id, {})
+ relation_obj.delete()
+ return custom_response(SUCCESS_CODE['3024'], response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class ApproveTaskAPIView(viewsets.ModelViewSet):
+ """approve junior by guardian"""
+ serializer_class = ApproveTaskSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('post',)
+ def create(self, request, *args, **kwargs):
+ """ Params
+ {"junior_id":"82",
+ "task_id":"43",
+ "action":"1"}
+ action 1 for approve
+ 2 for reject
+ """
+ # action 1 is use for approve and 2 for reject
+ try:
+ 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 guardian.guardian_code not in task_queryset.junior.guardian_code:
+ return custom_error_response(ERROR_CODE['2084'], response_status=status.HTTP_400_BAD_REQUEST)
+ 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": 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 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 task_queryset and task_queryset.task_status not in unexpected_task_status):
+ # save serializer
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3026'], response_status=status.HTTP_200_OK)
+ else:
+ return custom_error_response(ERROR_CODE['2038'], response_status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+#
+class GuardianListAPIView(viewsets.ModelViewSet):
+ """Guardian list of assosicated junior"""
+
+ serializer_class = GuardianDetailListSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+
+ def list(self, request, *args, **kwargs):
+ """ Guardian list of assosicated junior
+ No Params"""
+ try:
+ guardian_data = JuniorGuardianRelationship.objects.filter(junior__auth__email=self.request.user)
+ # fetch junior object
+ if guardian_data:
+ # use GuardianDetailListSerializer serializer
+ serializer = GuardianDetailListSerializer(guardian_data, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ return custom_response({"status": GUARDIAN_CODE_STATUS[1][0]}, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
diff --git a/junior/__init__.py b/junior/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/junior/admin.py b/junior/admin.py
new file mode 100644
index 0000000..2ffda51
--- /dev/null
+++ b/junior/admin.py
@@ -0,0 +1,52 @@
+"""Junior admin"""
+"""Third party Django app"""
+from django.contrib import admin
+"""Import Django app"""
+from .models import (Junior, JuniorPoints, JuniorGuardianRelationship, JuniorArticlePoints, JuniorArticle,
+ JuniorArticleCard, FAQ)
+# Register your models here.
+admin.site.register(FAQ)
+@admin.register(JuniorArticle)
+class JuniorArticleAdmin(admin.ModelAdmin):
+ """Junior Admin"""
+ list_display = ['junior', 'article', 'status', 'is_completed']
+
+ def __str__(self):
+ """Return email id"""
+ return self.junior__auth__email
+
+@admin.register(JuniorArticleCard)
+class JuniorArticleCardAdmin(admin.ModelAdmin):
+ """Junior Admin"""
+ list_display = ['junior', 'article', 'article_card', 'is_read']
+
+ def __str__(self):
+ """Return email id"""
+ return self.junior__auth__email
+@admin.register(Junior)
+class JuniorAdmin(admin.ModelAdmin):
+ """Junior Admin"""
+ list_display = ['auth', 'guardian_code']
+
+ def __str__(self):
+ """Return email id"""
+ return self.auth__email
+
+@admin.register(JuniorPoints)
+class JuniorPointsAdmin(admin.ModelAdmin):
+ """Junior Points Admin"""
+ list_display = ['junior', 'total_points', 'position']
+
+ def __str__(self):
+ """Return email id"""
+ return self.junior.auth.email
+
+@admin.register(JuniorGuardianRelationship)
+class JuniorGuardianRelationshipAdmin(admin.ModelAdmin):
+ """Junior Admin"""
+ list_display = ['guardian', 'junior', 'relationship']
+
+@admin.register(JuniorArticlePoints)
+class JuniorArticlePointsAdmin(admin.ModelAdmin):
+ """Junior Admin"""
+ list_display = ['junior', 'article', 'question', 'submitted_answer', 'is_answer_correct']
diff --git a/junior/apps.py b/junior/apps.py
new file mode 100644
index 0000000..f3df25e
--- /dev/null
+++ b/junior/apps.py
@@ -0,0 +1,8 @@
+"""App file"""
+"""Import AppConfig"""
+from django.apps import AppConfig
+
+class JuniorConfig(AppConfig):
+ """Junior config"""
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'junior'
diff --git a/junior/migrations/0001_initial.py b/junior/migrations/0001_initial.py
new file mode 100644
index 0000000..e451c2e
--- /dev/null
+++ b/junior/migrations/0001_initial.py
@@ -0,0 +1,42 @@
+# Generated by Django 4.2.2 on 2023-06-23 12:05
+
+from django.conf import settings
+import django.contrib.postgres.fields
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Junior',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('phone', models.CharField(blank=True, default=None, max_length=31, null=True)),
+ ('country_code', models.IntegerField(blank=True, null=True)),
+ ('gender', models.CharField(blank=True, choices=[('1', 'Male'), ('2', 'Female')], default=None, max_length=10, null=True)),
+ ('dob', models.DateField(blank=True, default=None, max_length=15, null=True)),
+ ('junior_code', models.CharField(blank=True, default=None, max_length=10, null=True)),
+ ('guardian_code', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, default=None, max_length=10, null=True), null=True, size=None)),
+ ('referral_code', models.CharField(blank=True, default=None, max_length=10, null=True)),
+ ('referral_code_used', models.CharField(blank=True, default=None, max_length=10, null=True)),
+ ('is_active', models.BooleanField(default=True)),
+ ('is_complete_profile', models.BooleanField(default=False)),
+ ('passcode', models.IntegerField(blank=True, default=None, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('auth', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='junior_profile', to=settings.AUTH_USER_MODEL, verbose_name='Email')),
+ ],
+ options={
+ 'verbose_name': 'Junior',
+ 'db_table': 'junior',
+ },
+ ),
+ ]
diff --git a/junior/migrations/0002_junior_country_name.py b/junior/migrations/0002_junior_country_name.py
new file mode 100644
index 0000000..0dd74bd
--- /dev/null
+++ b/junior/migrations/0002_junior_country_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-27 13:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='country_name',
+ field=models.CharField(blank=True, default=None, max_length=30, null=True),
+ ),
+ ]
diff --git a/junior/migrations/0003_junior_image.py b/junior/migrations/0003_junior_image.py
new file mode 100644
index 0000000..8a31d3f
--- /dev/null
+++ b/junior/migrations/0003_junior_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-28 10:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0002_junior_country_name'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='image',
+ field=models.ImageField(blank=True, default=None, null=True, upload_to='images/'),
+ ),
+ ]
diff --git a/junior/migrations/0004_alter_junior_image.py b/junior/migrations/0004_alter_junior_image.py
new file mode 100644
index 0000000..373436d
--- /dev/null
+++ b/junior/migrations/0004_alter_junior_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-29 06:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0003_junior_image'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='junior',
+ name='image',
+ field=models.ImageField(blank=True, default=None, null=True, upload_to=''),
+ ),
+ ]
diff --git a/junior/migrations/0005_junior_is_verified.py b/junior/migrations/0005_junior_is_verified.py
new file mode 100644
index 0000000..a5ed1e6
--- /dev/null
+++ b/junior/migrations/0005_junior_is_verified.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-29 12:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0004_alter_junior_image'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='is_verified',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/junior/migrations/0006_alter_junior_country_name.py b/junior/migrations/0006_alter_junior_country_name.py
new file mode 100644
index 0000000..e3db0ba
--- /dev/null
+++ b/junior/migrations/0006_alter_junior_country_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-06-30 10:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0005_junior_is_verified'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='junior',
+ name='country_name',
+ field=models.CharField(blank=True, default=None, max_length=100, null=True),
+ ),
+ ]
diff --git a/junior/migrations/0007_alter_junior_image.py b/junior/migrations/0007_alter_junior_image.py
new file mode 100644
index 0000000..be695a0
--- /dev/null
+++ b/junior/migrations/0007_alter_junior_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-06 12:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0006_alter_junior_country_name'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='junior',
+ name='image',
+ field=models.URLField(blank=True, default=None, null=True),
+ ),
+ ]
diff --git a/junior/migrations/0008_juniorpoints.py b/junior/migrations/0008_juniorpoints.py
new file mode 100644
index 0000000..d16b7e2
--- /dev/null
+++ b/junior/migrations/0008_juniorpoints.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.2 on 2023-07-09 12:40
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0007_alter_junior_image'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='JuniorPoints',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('total_task_points', models.IntegerField(blank=True, default=0, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('junior', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='junior_points', to='junior.junior')),
+ ],
+ options={
+ 'verbose_name': 'Junior Task Points',
+ 'db_table': 'junior_task_points',
+ },
+ ),
+ ]
diff --git a/junior/migrations/0009_juniorpoints_position.py b/junior/migrations/0009_juniorpoints_position.py
new file mode 100644
index 0000000..2f5b31d
--- /dev/null
+++ b/junior/migrations/0009_juniorpoints_position.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-10 07:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0008_juniorpoints'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='juniorpoints',
+ name='position',
+ field=models.IntegerField(blank=True, default=99999, null=True),
+ ),
+ ]
diff --git a/junior/migrations/0010_junior_signup_method.py b/junior/migrations/0010_junior_signup_method.py
new file mode 100644
index 0000000..a87d77a
--- /dev/null
+++ b/junior/migrations/0010_junior_signup_method.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-11 11:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0009_juniorpoints_position'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='signup_method',
+ field=models.CharField(choices=[('1', 'manual'), ('2', 'google'), ('3', 'apple')], default='1', max_length=31),
+ ),
+ ]
diff --git a/junior/migrations/0011_junior_relationship.py b/junior/migrations/0011_junior_relationship.py
new file mode 100644
index 0000000..45defb0
--- /dev/null
+++ b/junior/migrations/0011_junior_relationship.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-12 06:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0010_junior_signup_method'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='relationship',
+ field=models.CharField(blank=True, choices=[('1', 'parent'), ('2', 'legal_guardian')], default='1', max_length=31, null=True),
+ ),
+ ]
diff --git a/junior/migrations/0012_junior_is_invited.py b/junior/migrations/0012_junior_is_invited.py
new file mode 100644
index 0000000..f36f9b4
--- /dev/null
+++ b/junior/migrations/0012_junior_is_invited.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-12 10:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0011_junior_relationship'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='is_invited',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/junior/migrations/0013_alter_junior_options_alter_juniorpoints_options.py b/junior/migrations/0013_alter_junior_options_alter_juniorpoints_options.py
new file mode 100644
index 0000000..266d53c
--- /dev/null
+++ b/junior/migrations/0013_alter_junior_options_alter_juniorpoints_options.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.2 on 2023-07-14 09:34
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0012_junior_is_invited'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='junior',
+ options={'verbose_name': 'Junior', 'verbose_name_plural': 'Junior'},
+ ),
+ migrations.AlterModelOptions(
+ name='juniorpoints',
+ options={'verbose_name': 'Junior Task Points', 'verbose_name_plural': 'Junior Task Points'},
+ ),
+ ]
diff --git a/junior/migrations/0014_junior_is_password_set.py b/junior/migrations/0014_junior_is_password_set.py
new file mode 100644
index 0000000..a71e6c0
--- /dev/null
+++ b/junior/migrations/0014_junior_is_password_set.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-18 09:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0013_alter_junior_options_alter_juniorpoints_options'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='is_password_set',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/junior/migrations/0015_rename_total_task_points_juniorpoints_total_points.py b/junior/migrations/0015_rename_total_task_points_juniorpoints_total_points.py
new file mode 100644
index 0000000..038adba
--- /dev/null
+++ b/junior/migrations/0015_rename_total_task_points_juniorpoints_total_points.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-19 09:40
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0014_junior_is_password_set'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='juniorpoints',
+ old_name='total_task_points',
+ new_name='total_points',
+ ),
+ ]
diff --git a/junior/migrations/0016_juniorpoints_referral_points.py b/junior/migrations/0016_juniorpoints_referral_points.py
new file mode 100644
index 0000000..a70f45d
--- /dev/null
+++ b/junior/migrations/0016_juniorpoints_referral_points.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-19 11:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0015_rename_total_task_points_juniorpoints_total_points'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='juniorpoints',
+ name='referral_points',
+ field=models.IntegerField(blank=True, default=0, null=True),
+ ),
+ ]
diff --git a/junior/migrations/0017_juniorguardianrelationship.py b/junior/migrations/0017_juniorguardianrelationship.py
new file mode 100644
index 0000000..99d1707
--- /dev/null
+++ b/junior/migrations/0017_juniorguardianrelationship.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.2 on 2023-07-25 07:44
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('guardian', '0020_alter_juniortask_task_status'),
+ ('junior', '0016_juniorpoints_referral_points'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='JuniorGuardianRelationship',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('relationship', models.CharField(blank=True, choices=[('1', 'parent'), ('2', 'legal_guardian')], default='1', max_length=31, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('guardian', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guardian_relation', to='guardian.guardian', verbose_name='Guardian')),
+ ('junior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='junior_relation', to='junior.junior', verbose_name='Junior')),
+ ],
+ options={
+ 'verbose_name': 'Junior Guardian Relation',
+ 'verbose_name_plural': 'Junior Guardian Relation',
+ 'db_table': 'junior_guardian_relation',
+ },
+ ),
+ ]
diff --git a/junior/migrations/0018_remove_junior_relationship_and_more.py b/junior/migrations/0018_remove_junior_relationship_and_more.py
new file mode 100644
index 0000000..d24b43f
--- /dev/null
+++ b/junior/migrations/0018_remove_junior_relationship_and_more.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.2 on 2023-08-02 11:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0017_juniorguardianrelationship'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='junior',
+ name='relationship',
+ ),
+ migrations.AddField(
+ model_name='junior',
+ name='guardian_code_approved',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/junior/migrations/0019_juniorarticlepoints.py b/junior/migrations/0019_juniorarticlepoints.py
new file mode 100644
index 0000000..1112702
--- /dev/null
+++ b/junior/migrations/0019_juniorarticlepoints.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.2.2 on 2023-08-07 13:29
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('web_admin', '0004_alter_surveyoption_survey'),
+ ('junior', '0018_remove_junior_relationship_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='JuniorArticlePoints',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('earn_points', models.IntegerField(blank=True, default=5, null=True)),
+ ('is_attempt', models.BooleanField(default=False)),
+ ('is_answer_correct', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='junior_articles', to='web_admin.article')),
+ ('junior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='juniors_details', to='junior.junior', verbose_name='Junior')),
+ ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='web_admin.articlesurvey')),
+ ('submitted_answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submitted_answer', to='web_admin.surveyoption')),
+ ],
+ ),
+ ]
diff --git a/junior/migrations/0020_junior_guardian_code_status.py b/junior/migrations/0020_junior_guardian_code_status.py
new file mode 100644
index 0000000..62e09d3
--- /dev/null
+++ b/junior/migrations/0020_junior_guardian_code_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-08-08 05:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0019_juniorarticlepoints'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='guardian_code_status',
+ field=models.CharField(blank=True, choices=[('1', 'no guardian code'), ('2', 'exist guardian code'), ('3', 'request for guardian code')], default='1', max_length=31, null=True),
+ ),
+ ]
diff --git a/junior/migrations/0021_alter_juniorarticlepoints_submitted_answer.py b/junior/migrations/0021_alter_juniorarticlepoints_submitted_answer.py
new file mode 100644
index 0000000..45ce9a4
--- /dev/null
+++ b/junior/migrations/0021_alter_juniorarticlepoints_submitted_answer.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.2.2 on 2023-08-08 09:45
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('web_admin', '0004_alter_surveyoption_survey'),
+ ('junior', '0020_junior_guardian_code_status'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='juniorarticlepoints',
+ name='submitted_answer',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_answer', to='web_admin.surveyoption'),
+ ),
+ ]
diff --git a/junior/migrations/0022_juniorarticle.py b/junior/migrations/0022_juniorarticle.py
new file mode 100644
index 0000000..9dd794e
--- /dev/null
+++ b/junior/migrations/0022_juniorarticle.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.2.2 on 2023-08-09 09:34
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('web_admin', '0004_alter_surveyoption_survey'),
+ ('junior', '0021_alter_juniorarticlepoints_submitted_answer'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='JuniorArticle',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_completed', models.BooleanField(default=False)),
+ ('status', models.CharField(blank=True, choices=[('1', 'read'), ('2', 'in_progress'), ('3', 'completed')], default='1', max_length=10, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='junior_articles_details', to='web_admin.article')),
+ ('junior', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='juniors_article', to='junior.junior', verbose_name='Junior')),
+ ],
+ ),
+ ]
diff --git a/junior/migrations/0023_juniorarticlecard.py b/junior/migrations/0023_juniorarticlecard.py
new file mode 100644
index 0000000..9b8a1af
--- /dev/null
+++ b/junior/migrations/0023_juniorarticlecard.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.2.2 on 2023-08-09 10:47
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('web_admin', '0004_alter_surveyoption_survey'),
+ ('junior', '0022_juniorarticle'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='JuniorArticleCard',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_read', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='junior_articles_detail', to='web_admin.article')),
+ ('article_card', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='junior_article_card', to='web_admin.articlecard')),
+ ('junior', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='juniors_article_card', to='junior.junior', verbose_name='Junior')),
+ ],
+ ),
+ ]
diff --git a/junior/migrations/0024_juniorarticle_current_card_page_and_more.py b/junior/migrations/0024_juniorarticle_current_card_page_and_more.py
new file mode 100644
index 0000000..b00eba9
--- /dev/null
+++ b/junior/migrations/0024_juniorarticle_current_card_page_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.2 on 2023-08-10 08:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0023_juniorarticlecard'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='juniorarticle',
+ name='current_card_page',
+ field=models.IntegerField(blank=True, default=0, null=True),
+ ),
+ migrations.AddField(
+ model_name='juniorarticle',
+ name='current_que_page',
+ field=models.IntegerField(blank=True, default=0, null=True),
+ ),
+ ]
diff --git a/junior/migrations/0025_alter_juniorarticle_junior.py b/junior/migrations/0025_alter_juniorarticle_junior.py
new file mode 100644
index 0000000..26c7966
--- /dev/null
+++ b/junior/migrations/0025_alter_juniorarticle_junior.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.2 on 2023-08-10 14:46
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0024_juniorarticle_current_card_page_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='juniorarticle',
+ name='junior',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='juniors_article', to='junior.junior', verbose_name='Junior'),
+ ),
+ ]
diff --git a/junior/migrations/0026_faq_alter_juniorarticle_options_and_more.py b/junior/migrations/0026_faq_alter_juniorarticle_options_and_more.py
new file mode 100644
index 0000000..12243a2
--- /dev/null
+++ b/junior/migrations/0026_faq_alter_juniorarticle_options_and_more.py
@@ -0,0 +1,39 @@
+# Generated by Django 4.2.2 on 2023-08-17 09:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0025_alter_juniorarticle_junior'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='FAQ',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('question', models.IntegerField(max_length=100)),
+ ('description', models.CharField(max_length=500)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': 'FAQ',
+ 'verbose_name_plural': 'FAQ',
+ },
+ ),
+ migrations.AlterModelOptions(
+ name='juniorarticle',
+ options={'verbose_name': 'Junior Article', 'verbose_name_plural': 'Junior Article'},
+ ),
+ migrations.AlterModelOptions(
+ name='juniorarticlecard',
+ options={'verbose_name': 'Junior Article Card', 'verbose_name_plural': 'Junior Article Card'},
+ ),
+ migrations.AlterModelOptions(
+ name='juniorarticlepoints',
+ options={'verbose_name': 'Junior Article Points', 'verbose_name_plural': 'Junior Article Points'},
+ ),
+ ]
diff --git a/junior/migrations/0027_alter_faq_question.py b/junior/migrations/0027_alter_faq_question.py
new file mode 100644
index 0000000..46fefd8
--- /dev/null
+++ b/junior/migrations/0027_alter_faq_question.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-08-17 09:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0026_faq_alter_juniorarticle_options_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='faq',
+ name='question',
+ field=models.CharField(max_length=100),
+ ),
+ ]
diff --git a/junior/migrations/0028_faq_status.py b/junior/migrations/0028_faq_status.py
new file mode 100644
index 0000000..bdfbf67
--- /dev/null
+++ b/junior/migrations/0028_faq_status.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-08-17 10:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0027_alter_faq_question'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='faq',
+ name='status',
+ field=models.IntegerField(blank=True, default=1, null=True),
+ ),
+ ]
diff --git a/junior/migrations/0029_junior_is_deleted.py b/junior/migrations/0029_junior_is_deleted.py
new file mode 100644
index 0000000..a39f60f
--- /dev/null
+++ b/junior/migrations/0029_junior_is_deleted.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-08-17 12:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0028_faq_status'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='is_deleted',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/junior/migrations/0030_remove_junior_guardian_code_status.py b/junior/migrations/0030_remove_junior_guardian_code_status.py
new file mode 100644
index 0000000..6949e9a
--- /dev/null
+++ b/junior/migrations/0030_remove_junior_guardian_code_status.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.2 on 2023-08-26 08:59
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0029_junior_is_deleted'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='junior',
+ name='guardian_code_status',
+ ),
+ ]
diff --git a/junior/migrations/0031_junior_guardian_code_status.py b/junior/migrations/0031_junior_guardian_code_status.py
new file mode 100644
index 0000000..c342f28
--- /dev/null
+++ b/junior/migrations/0031_junior_guardian_code_status.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.2 on 2023-08-26 08:59
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('junior', '0030_remove_junior_guardian_code_status'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='junior',
+ name='guardian_code_status',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, default=None, max_length=10, null=True), null=True, size=None),
+ ),
+ ]
diff --git a/junior/migrations/__init__.py b/junior/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/junior/models.py b/junior/models.py
new file mode 100644
index 0000000..559acfe
--- /dev/null
+++ b/junior/models.py
@@ -0,0 +1,244 @@
+"""Junior model """
+"""Import django"""
+from django.db import models
+"""Import get_user_model function"""
+from django.contrib.auth import get_user_model
+"""Import ArrayField"""
+from django.contrib.postgres.fields import ArrayField
+"""Import django app"""
+from base.constants import GENDERS, SIGNUP_METHODS, RELATIONSHIP, GUARDIAN_CODE_STATUS, ARTICLE_STATUS
+# Import guardian's model
+from guardian.models import Guardian
+# Import web admin's model
+from web_admin.models import SurveyOption, ArticleSurvey, Article, ArticleCard
+"""Define User model"""
+User = get_user_model()
+# Create your models here.
+# Define junior model with
+# various fields like
+# phone, country code,
+# country name,
+# gender,
+# date of birth,
+# profile image,
+# relationship type of the guardian
+# signup method,
+# guardian code,
+# junior code,
+# referral code,
+# referral code that used by the junior
+# is invited junior
+# profile is active or not
+# profile is complete or not
+# passcode
+# junior is verified or not
+"""Define junior points model"""
+# points of the junior
+# position of the junior
+# define junior guardian relation model
+class Junior(models.Model):
+ """Junior model"""
+ auth = models.ForeignKey(User, on_delete=models.CASCADE, related_name='junior_profile', verbose_name='Email')
+ # Contact details"""
+ phone = models.CharField(max_length=31, null=True, blank=True, default=None)
+ country_code = models.IntegerField(blank=True, null=True)
+ # country name of the guardian"""
+ country_name = models.CharField(max_length=100, null=True, blank=True, default=None)
+ # Personal info"""
+ gender = models.CharField(max_length=10, choices=GENDERS, null=True, blank=True, default=None)
+ # Date of birth"""
+ dob = models.DateField(max_length=15, null=True, blank=True, default=None)
+ # Image of the junior"""
+ image = models.URLField(null=True, blank=True, default=None)
+ # Sign up method"""
+ signup_method = models.CharField(max_length=31, choices=SIGNUP_METHODS, default='1')
+ # Codes"""
+ junior_code = models.CharField(max_length=10, null=True, blank=True, default=None)
+ # Guardian Codes"""
+ guardian_code = ArrayField(models.CharField(max_length=10, null=True, blank=True, default=None),null=True)
+ # Referral code"""
+ referral_code = models.CharField(max_length=10, null=True, blank=True, default=None)
+ # Referral code that is used by junior while signup"""
+ referral_code_used = models.CharField(max_length=10, null=True, blank=True, default=None)
+ # invited junior"""
+ is_invited = models.BooleanField(default=False)
+ # Profile activity"""
+ is_active = models.BooleanField(default=True)
+ # check password is set or not
+ is_password_set = models.BooleanField(default=True)
+ # junior profile is complete or not"""
+ is_complete_profile = models.BooleanField(default=False)
+ # junior profile deleted or not"""
+ is_deleted = models.BooleanField(default=False)
+ # passcode of the junior profile"""
+ passcode = models.IntegerField(null=True, blank=True, default=None)
+ # junior is verified or not"""
+ is_verified = models.BooleanField(default=False)
+ """guardian code is approved or not"""
+ guardian_code_approved = models.BooleanField(default=False)
+ # # guardian code status"""
+ guardian_code_status = ArrayField(models.CharField(max_length=10, null=True, blank=True, default=None), null=True,
+ )
+ # Profile created and updated time"""
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(object):
+ """ Meta class """
+ db_table = 'junior'
+ verbose_name = 'Junior'
+ # another name of the model"""
+ verbose_name_plural = 'Junior'
+
+ def __str__(self):
+ """Return email id"""
+ return f'{self.auth}'
+
+class JuniorPoints(models.Model):
+ """Junior model"""
+ junior = models.OneToOneField(Junior, on_delete=models.CASCADE, related_name='junior_points')
+ # Total earned points"""
+ total_points = models.IntegerField(blank=True, null=True, default=0)
+ # referral points"""
+ referral_points = models.IntegerField(blank=True, null=True, default=0)
+ # position of the junior"""
+ position = models.IntegerField(blank=True, null=True, default=99999)
+ # Profile created and updated time"""
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(object):
+ """ Meta class """
+ db_table = 'junior_task_points'
+ verbose_name = 'Junior Task Points'
+ # another name of the model"""
+ verbose_name_plural = 'Junior Task Points'
+
+ def __str__(self):
+ """Return email id"""
+ return f'{self.junior.auth}'
+
+class JuniorGuardianRelationship(models.Model):
+ """Junior Guardian relationship model"""
+ guardian = models.ForeignKey(Guardian, on_delete=models.CASCADE, related_name='guardian_relation',
+ verbose_name='Guardian')
+ # associated junior with the task
+ junior = models.ForeignKey(Junior, on_delete=models.CASCADE, related_name='junior_relation', verbose_name='Junior')
+ # relation between guardian and junior"""
+ relationship = models.CharField(max_length=31, choices=RELATIONSHIP, null=True, blank=True,
+ default='1')
+ """Profile created and updated time"""
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta(object):
+ """ Meta class """
+ db_table = 'junior_guardian_relation'
+ """verbose name of the model"""
+ verbose_name = 'Junior Guardian Relation'
+ verbose_name_plural = 'Junior Guardian Relation'
+
+ def __str__(self):
+ """Return email id"""
+ return f'{self.guardian.user}'
+
+
+class JuniorArticlePoints(models.Model):
+ """
+ Survey Options model
+ """
+ # associated junior with the task
+ junior = models.ForeignKey(Junior, on_delete=models.CASCADE, related_name='juniors_details', verbose_name='Junior')
+ article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='junior_articles')
+ question = models.ForeignKey(ArticleSurvey, on_delete=models.CASCADE, related_name='questions')
+ submitted_answer = models.ForeignKey(SurveyOption, on_delete=models.SET_NULL, null=True,
+ related_name='submitted_answer')
+ # earn points"""
+ earn_points = models.IntegerField(blank=True, null=True, default=5)
+ is_attempt = models.BooleanField(default=False)
+ is_answer_correct = models.BooleanField(default=False)
+ 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}'
+
+class JuniorArticle(models.Model):
+ """
+ Survey Options model
+ """
+ # associated junior with the task
+ junior = models.ForeignKey(Junior, on_delete=models.CASCADE, related_name='juniors_article',
+ verbose_name='Junior')
+ article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='junior_articles_details')
+ # article completed"""
+ is_completed = models.BooleanField(default=False)
+ status = models.CharField(max_length=10, choices=ARTICLE_STATUS, null=True, blank=True, default='1')
+ current_card_page = models.IntegerField(blank=True, null=True, default=0)
+ current_que_page = models.IntegerField(blank=True, null=True, default=0)
+ 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}'
+
+class JuniorArticleCard(models.Model):
+ """
+ Survey Options model
+ """
+ # associated junior with the task
+ junior = models.ForeignKey(Junior, on_delete=models.CASCADE, related_name='juniors_article_card',
+ verbose_name='Junior')
+ article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='junior_articles_detail')
+ article_card = models.ForeignKey(ArticleCard, on_delete=models.CASCADE, related_name='junior_article_card')
+
+ # article card read"""
+ is_read = models.BooleanField(default=False)
+ 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
new file mode 100644
index 0000000..fadb55f
--- /dev/null
+++ b/junior/serializers.py
@@ -0,0 +1,550 @@
+"""Serializer file for junior"""
+# third party imports
+import pytz
+
+# django imports
+from rest_framework import serializers
+from django.contrib.auth.models import User
+from django.db import transaction
+from datetime import datetime
+from django.utils import timezone
+from rest_framework_simplejwt.tokens import RefreshToken
+
+# local imports
+from account.utils import send_otp_email, generate_code, make_special_password
+from junior.models import Junior, JuniorPoints, JuniorGuardianRelationship, JuniorArticlePoints, FAQ
+from guardian.tasks import generate_otp
+from base.messages import ERROR_CODE, SUCCESS_CODE
+from base.constants import (PENDING, IN_PROGRESS, REJECTED, REQUESTED, COMPLETED, NUMBER, JUN, ZOD, EXPIRED,
+ GUARDIAN_CODE_STATUS, JUNIOR, GUARDIAN)
+from guardian.models import Guardian, JuniorTask
+from account.models import UserEmailOtp, UserNotification
+from junior.utils import junior_notification_email, junior_approval_mail, get_junior_leaderboard_rank
+from guardian.utils import real_time, update_referral_points, convert_timedelta_into_datetime
+from notifications.utils import send_notification
+from notifications.constants import (ASSOCIATE_REQUEST, ASSOCIATE_JUNIOR, TASK_ACTION,
+ )
+from web_admin.models import ArticleCard
+
+class ListCharField(serializers.ListField):
+ """Serializer for Array field"""
+ child = serializers.CharField()
+
+ def to_representation(self, data):
+ """to represent the data"""
+ return data
+
+ def to_internal_value(self, data):
+ """internal value"""
+ if isinstance(data, list):
+ return data
+ raise serializers.ValidationError({"details":ERROR_CODE['2025']})
+
+
+class CreateJuniorSerializer(serializers.ModelSerializer):
+ """Create junior serializer"""
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+ email = serializers.SerializerMethodField('get_email')
+ phone = serializers.CharField(max_length=20, required=False)
+ country_code = serializers.IntegerField(required=False)
+ dob = serializers.DateField(required=False)
+ referral_code = serializers.CharField(max_length=100, required=False)
+ guardian_code = ListCharField(required=False)
+ image = serializers.URLField(required=False)
+
+ class Meta(object):
+ """Meta info"""
+ model = Junior
+ fields = ['id', 'first_name', 'last_name', 'email', 'phone', 'gender', 'country_code', 'dob', 'referral_code',
+ 'passcode', 'is_complete_profile', 'guardian_code', 'referral_code_used',
+ 'country_name', 'image', 'is_invited']
+
+ def get_first_name(self,obj):
+ """first name of junior"""
+ return obj.auth.first_name
+
+ def get_last_name(self,obj):
+ """last name of junior"""
+ return obj.auth.last_name
+
+ def get_email(self,obj):
+ """email of junior"""
+ return obj.auth.email
+
+ def create(self, validated_data):
+ """Create junior profile"""
+ guardian_code = validated_data.get('guardian_code',None)
+ user = User.objects.filter(username=self.context['user']).last()
+ if user:
+ """Save first and last name of junior"""
+ if self.context.get('first_name') != '' and self.context.get('first_name') is not None:
+ user.first_name = self.context.get('first_name')
+ if self.context.get('last_name') != '' and self.context.get('last_name') is not None:
+ user.last_name = self.context.get('last_name')
+ user.save()
+ """Create junior data"""
+ junior = Junior.objects.filter(auth=self.context['user']).last()
+ if junior:
+ """update details according to the data get from request"""
+ junior.gender = validated_data.get('gender',junior.gender)
+ # Update guardian code"""
+ # condition for guardian code
+ if guardian_code:
+ if junior.guardian_code and guardian_code:
+ if guardian_code[0] in junior.guardian_code:
+ raise serializers.ValidationError({"error":ERROR_CODE['2076'],"code":"400", "status":"failed"})
+ if not junior.guardian_code:
+ junior.guardian_code = []
+ junior.guardian_code_status = []
+ junior.guardian_code.extend(guardian_code)
+ junior.guardian_code_status.extend(str(NUMBER['three']))
+ elif len(junior.guardian_code) < 3 and len(guardian_code) < 3:
+ junior.guardian_code.extend(guardian_code)
+ junior.guardian_code_status.extend(str(NUMBER['three']))
+ else:
+ raise serializers.ValidationError({"error":ERROR_CODE['2081'],"code":"400", "status":"failed"})
+ guardian_data = Guardian.objects.filter(guardian_code=guardian_code[0]).last()
+ if guardian_data:
+ JuniorGuardianRelationship.objects.get_or_create(guardian=guardian_data, junior=junior)
+ send_notification.delay(ASSOCIATE_REQUEST, junior.auth_id, JUNIOR, guardian_data.user_id, {})
+ # junior_approval_mail.delay(user.email, user.first_name) removed as per changes
+ 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)
+ """Update country code and phone number"""
+ junior.phone = validated_data.get('phone', junior.phone)
+ junior.country_code = validated_data.get('country_code', junior.country_code)
+ junior.image = validated_data.get('image', junior.image)
+ """Complete profile of the junior if below all data are filled"""
+ complete_profile_field = all([junior.gender, junior.country_name, junior.image,
+ junior.dob, user.first_name])
+ junior.is_complete_profile = False
+ if complete_profile_field:
+ junior.is_complete_profile = True
+ referral_code_used = validated_data.get('referral_code_used')
+ update_referral_points(junior.referral_code, referral_code_used)
+ junior.referral_code_used = validated_data.get('referral_code_used', junior.referral_code_used)
+ junior.save()
+ return junior
+
+ def save(self, **kwargs):
+ """Save the data into junior table"""
+ with transaction.atomic():
+ instance = super().save(**kwargs)
+ return instance
+
+class JuniorDetailSerializer(serializers.ModelSerializer):
+ """junior serializer"""
+ email = serializers.SerializerMethodField('get_auth')
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+
+ def get_auth(self, obj):
+ """user email address"""
+ return obj.auth.username
+
+ def get_first_name(self, obj):
+ """user first name"""
+ return obj.auth.first_name
+
+ def get_last_name(self, obj):
+ """user last name"""
+ return obj.auth.last_name
+
+ class Meta(object):
+ """Meta info"""
+ 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', 'is_deleted', 'updated_at']
+
+class JuniorDetailListSerializer(serializers.ModelSerializer):
+ """junior serializer"""
+
+ email = serializers.SerializerMethodField('get_auth')
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+ assigned_task = serializers.SerializerMethodField('get_assigned_task')
+ points = serializers.SerializerMethodField('get_points')
+ in_progress_task = serializers.SerializerMethodField('get_in_progress_task')
+ completed_task = serializers.SerializerMethodField('get_completed_task')
+ requested_task = serializers.SerializerMethodField('get_requested_task')
+ rejected_task = serializers.SerializerMethodField('get_rejected_task')
+ pending_task = serializers.SerializerMethodField('get_pending_task')
+ position = serializers.SerializerMethodField('get_position')
+ guardian_code_status = serializers.SerializerMethodField('get_guardian_code_status')
+
+
+ def get_auth(self, obj):
+ return obj.auth.username
+
+ def get_first_name(self, obj):
+ return obj.auth.first_name
+
+ def get_last_name(self, obj):
+ return obj.auth.last_name
+
+ def get_assigned_task(self, obj):
+ data = JuniorTask.objects.filter(junior=obj).count()
+ return data
+
+ def get_position(self, obj):
+ return get_junior_leaderboard_rank(obj)
+
+ def get_points(self, obj):
+ data = JuniorPoints.objects.filter(junior=obj).last()
+ if data:
+ return data.total_points
+ return NUMBER['zero']
+
+ def get_in_progress_task(self, obj):
+ data = JuniorTask.objects.filter(junior=obj, task_status=IN_PROGRESS).count()
+ return data
+
+ def get_completed_task(self, obj):
+ data = JuniorTask.objects.filter(junior=obj, task_status=COMPLETED).count()
+ return data
+
+
+ def get_requested_task(self, obj):
+ data = JuniorTask.objects.filter(junior=obj, task_status=REQUESTED).count()
+ return data
+
+ def get_rejected_task(self, obj):
+ data = JuniorTask.objects.filter(junior=obj, task_status=REJECTED).count()
+ return data
+
+ def get_pending_task(self, obj):
+ data = JuniorTask.objects.filter(junior=obj, task_status=PENDING).count()
+ return data
+
+ def get_guardian_code_status(self, obj):
+ if self.context['guardian_code'] in obj.guardian_code:
+ index = obj.guardian_code.index(self.context['guardian_code'])
+ if obj.guardian_code_status:
+ data = obj.guardian_code_status[index]
+ return data
+ class Meta(object):
+ """Meta info"""
+ model = Junior
+ fields = ['id', 'email', 'first_name', 'last_name', 'country_code', 'country_name', 'phone', 'gender', 'dob',
+ 'guardian_code', 'referral_code','is_active', 'is_complete_profile', 'created_at', 'image',
+ 'updated_at', 'assigned_task','points', 'pending_task', 'in_progress_task', 'completed_task',
+ 'requested_task', 'rejected_task', 'position', 'is_invited', 'guardian_code_status',
+ 'is_deleted']
+
+class JuniorProfileSerializer(serializers.ModelSerializer):
+ """junior serializer"""
+ email = serializers.SerializerMethodField('get_auth')
+ first_name = serializers.SerializerMethodField('get_first_name')
+ last_name = serializers.SerializerMethodField('get_last_name')
+ notification_count = serializers.SerializerMethodField('get_notification_count')
+ total_count = serializers.SerializerMethodField('get_total_count')
+ complete_field_count = serializers.SerializerMethodField('get_complete_field_count')
+
+ def get_auth(self, obj):
+ """user email address"""
+ return obj.auth.username
+
+ def get_first_name(self, obj):
+ """user first name"""
+ return obj.auth.first_name
+
+ def get_last_name(self, obj):
+ """user last name"""
+ return obj.auth.last_name
+
+ def get_notification_count(self, obj):
+ """total notification count"""
+ return 0
+
+ def get_total_count(self, obj):
+ """total fields count"""
+ return NUMBER['five']
+
+ def get_complete_field_count(self, obj):
+ """total filled fields count"""
+ field_list = [obj.auth.first_name, obj.country_name,
+ obj.gender, obj.dob, obj.image]
+ complete_field = [data for data in field_list if data is not None and data != '']
+ return len(complete_field)
+ class Meta(object):
+ """Meta info"""
+ model = Junior
+ 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_deleted']
+
+class AddJuniorSerializer(serializers.ModelSerializer):
+ """Add junior serializer"""
+ class Meta(object):
+ """Meta info"""
+ model = Junior
+ fields = ['id', 'gender', 'dob', 'is_invited']
+
+
+ def create(self, validated_data):
+ """ create junior"""
+ with transaction.atomic():
+ email = self.context['email']
+ guardian = self.context['user']
+ relationship = self.context['relationship']
+ profile_image = self.context['image']
+ full_name = self.context['first_name'] + ' ' + self.context['last_name']
+ guardian_data = Guardian.objects.filter(user__username=guardian).last()
+ user_data = User.objects.create(username=email, email=email,
+ first_name=self.context['first_name'],
+ last_name=self.context['last_name'])
+ special_password = make_special_password()
+ user_data.set_password(special_password)
+ user_data.save()
+ junior_data = Junior.objects.create(auth=user_data, gender=validated_data.get('gender'),
+ image=profile_image,
+ dob=validated_data.get('dob'), is_invited=True,
+ guardian_code=[guardian_data.guardian_code],
+ junior_code=generate_code(JUN, user_data.id),
+ referral_code=generate_code(ZOD, user_data.id),
+ referral_code_used=guardian_data.referral_code,
+ is_password_set=False, is_verified=True,
+ guardian_code_status=[str(NUMBER['two'])])
+ JuniorGuardianRelationship.objects.create(guardian=guardian_data, junior=junior_data,
+ relationship=relationship)
+ total_junior = Junior.objects.all().count()
+ JuniorPoints.objects.create(junior=junior_data, position=total_junior)
+ """Generate otp"""
+ otp_value = generate_otp()
+ expiry_time = timezone.now() + timezone.timedelta(days=1)
+ UserEmailOtp.objects.create(email=email, otp=otp_value,
+ user_type='1', expired_at=expiry_time, is_verified=True)
+ # add push notification
+ UserNotification.objects.get_or_create(user=user_data)
+ """Notification email"""
+ junior_notification_email.delay(email, full_name, email, special_password)
+ # push notification
+ send_notification.delay(ASSOCIATE_JUNIOR, None, GUARDIAN, junior_data.auth_id, {})
+ return junior_data
+
+
+
+class RemoveJuniorSerializer(serializers.ModelSerializer):
+ """User Update Serializer"""
+ class Meta(object):
+ """Meta class"""
+ model = Junior
+ fields = ('id', 'is_invited')
+ def update(self, instance, validated_data):
+ if instance:
+ guardian_code = self.context['guardian_code']
+ instance.is_invited = False
+ if instance.guardian_code and ('-' in instance.guardian_code):
+ instance.guardian_code.remove('-')
+ index = instance.guardian_code.index(guardian_code)
+ instance.guardian_code.remove(guardian_code)
+ instance.guardian_code_status.pop(index)
+ instance.save()
+ return instance
+
+
+class CompleteTaskSerializer(serializers.ModelSerializer):
+ """User task Serializer"""
+ class Meta(object):
+ """Meta class"""
+ model = JuniorTask
+ fields = ('id', 'image')
+ def update(self, instance, validated_data):
+ instance.image = validated_data.get('image', instance.image)
+ instance.requested_on = timezone.now().astimezone(pytz.utc)
+ instance.task_status = str(NUMBER['four'])
+ instance.is_approved = False
+ instance.save()
+ send_notification.delay(TASK_ACTION, instance.junior.auth_id, JUNIOR,
+ instance.guardian.user_id, {'task_id': instance.id})
+ return instance
+
+class JuniorPointsSerializer(serializers.ModelSerializer):
+ """Junior points serializer"""
+ junior_id = serializers.SerializerMethodField('get_junior_id')
+ total_points = serializers.SerializerMethodField('get_points')
+ in_progress_task = serializers.SerializerMethodField('get_in_progress_task')
+ completed_task = serializers.SerializerMethodField('get_completed_task')
+ requested_task = serializers.SerializerMethodField('get_requested_task')
+ rejected_task = serializers.SerializerMethodField('get_rejected_task')
+ pending_task = serializers.SerializerMethodField('get_pending_task')
+ expired_task = serializers.SerializerMethodField('get_expired_task')
+ position = serializers.SerializerMethodField('get_position')
+
+ def get_junior_id(self, obj):
+ """junior id"""
+ return obj.junior.id
+
+ def get_position(self, obj):
+ return get_junior_leaderboard_rank(obj.junior)
+ def get_points(self, obj):
+ """total points"""
+ points = JuniorPoints.objects.filter(junior=obj.junior).last()
+ if points:
+ return points.total_points
+
+ def get_in_progress_task(self, obj):
+ return JuniorTask.objects.filter(junior=obj.junior, task_status=IN_PROGRESS).count()
+
+ def get_completed_task(self, obj):
+ return JuniorTask.objects.filter(junior=obj.junior, task_status=COMPLETED).count()
+
+
+ def get_requested_task(self, obj):
+ return JuniorTask.objects.filter(junior=obj.junior, task_status=REQUESTED).count()
+
+
+ def get_rejected_task(self, obj):
+ return JuniorTask.objects.filter(junior=obj.junior, task_status=REJECTED).count()
+
+
+ def get_pending_task(self, obj):
+ return JuniorTask.objects.filter(junior=obj.junior, task_status=PENDING).count()
+
+ def get_expired_task(self, obj):
+ return JuniorTask.objects.filter(junior=obj.junior, task_status=EXPIRED).count()
+
+ class Meta(object):
+ """Meta info"""
+ model = Junior
+ fields = ['junior_id', 'total_points', 'position', 'pending_task', 'in_progress_task', 'completed_task',
+ 'requested_task', 'rejected_task', 'expired_task', 'is_deleted']
+
+class AddGuardianSerializer(serializers.ModelSerializer):
+ """Add guardian serializer"""
+
+ class Meta(object):
+ """Meta info"""
+ model = Guardian
+ fields = ['id']
+
+
+ def create(self, validated_data):
+ """ invite and create guardian"""
+ with transaction.atomic():
+ email = self.context['email']
+ junior = self.context['user']
+ relationship = self.context['relationship']
+ full_name = self.context['first_name'] + ' ' + self.context['last_name']
+ junior_data = Junior.objects.filter(auth__username=junior).last()
+ junior_data.guardian_code_status = GUARDIAN_CODE_STATUS[2][0]
+ junior_data.save()
+ instance = User.objects.filter(username=email).last()
+ if instance:
+ guardian_data = Guardian.objects.filter(user=instance).update(is_invited=True,
+ referral_code=generate_code(ZOD,
+ instance.id),
+ referral_code_used=junior_data.referral_code,
+ is_verified=True)
+ UserNotification.objects.get_or_create(user=instance)
+ return guardian_data
+ else:
+ user = User.objects.create(username=email, email=email,
+ first_name=self.context['first_name'],
+ last_name=self.context['last_name'])
+
+ password = User.objects.make_random_password()
+ user.set_password(password)
+ user.save()
+ guardian_data = Guardian.objects.create(user=user, is_invited=True,
+ referral_code=generate_code(ZOD, user.id),
+ referral_code_used=junior_data.referral_code,
+ is_password_set=False, is_verified=True)
+ """Generate otp"""
+ otp_value = generate_otp()
+ expiry_time = timezone.now() + timezone.timedelta(days=1)
+ UserEmailOtp.objects.create(email=email, otp=otp_value,
+ user_type=str(NUMBER['two']), expired_at=expiry_time,
+ is_verified=True)
+ UserNotification.objects.get_or_create(user=user)
+ JuniorGuardianRelationship.objects.create(guardian=guardian_data, junior=junior_data,
+ relationship=relationship)
+
+ """Notification email"""
+ junior_notification_email(email, full_name, email, password)
+ # junior_approval_mail.delay(email, full_name) removed as per changes
+ send_notification.delay(ASSOCIATE_REQUEST, junior_data.auth_id, JUNIOR, guardian_data.user_id, {})
+ return guardian_data
+
+class StartTaskSerializer(serializers.ModelSerializer):
+ """User task Serializer"""
+ task_duration = serializers.SerializerMethodField('get_task_duration')
+
+ def get_task_duration(self, obj):
+ """ remaining time to complete task"""
+ due_date = datetime.combine(obj.due_date, datetime.max.time())
+ # fetch real time
+ # real_datetime = real_time()
+ # new code
+ due_date = due_date.astimezone(pytz.utc)
+ real_datetime = timezone.now().astimezone(pytz.utc)
+ # Perform the subtraction
+ if due_date > real_datetime:
+ time_difference = due_date - real_datetime
+ time_only = convert_timedelta_into_datetime(time_difference)
+ return str(time_difference.days) + ' days ' + str(time_only)
+ return str(NUMBER['zero']) + ' days ' + '00:00:00:00000'
+ class Meta(object):
+ """Meta class"""
+ model = JuniorTask
+ fields = ('id', 'task_duration')
+ def update(self, instance, validated_data):
+ instance.task_status = str(NUMBER['two'])
+ instance.save()
+ return instance
+
+class ReAssignTaskSerializer(serializers.ModelSerializer):
+ """User task Serializer"""
+ class Meta(object):
+ """Meta class"""
+ model = JuniorTask
+ fields = ('id', 'due_date')
+ def update(self, instance, validated_data):
+ instance.task_status = str(NUMBER['one'])
+ instance.due_date = validated_data.get('due_date')
+ instance.is_approved = False
+ instance.requested_on = None
+ instance.save()
+ return instance
+
+class RemoveGuardianCodeSerializer(serializers.ModelSerializer):
+ """User task Serializer"""
+ class Meta(object):
+ """Meta class"""
+ model = Junior
+ fields = ('id', )
+ def update(self, instance, validated_data):
+ guardian_code = self.context['guardian_code']
+ if guardian_code in instance.guardian_code:
+ if instance.guardian_code and ('-' in instance.guardian_code):
+ instance.guardian_code.remove('-')
+ if instance.guardian_code_status and ('-' in instance.guardian_code_status):
+ instance.guardian_code_status.remove('-')
+ index = instance.guardian_code.index(guardian_code)
+ instance.guardian_code.remove(guardian_code)
+ instance.guardian_code_status.pop(index)
+ else:
+ raise serializers.ValidationError({"error":ERROR_CODE['2082'],"code":"400", "status":"failed"})
+ instance.save()
+ return instance
+
+class FAQSerializer(serializers.ModelSerializer):
+ """FAQ Serializer"""
+
+ class Meta(object):
+ """meta info"""
+ model = FAQ
+ fields = ('id', 'question', 'description')
+
+class CreateArticleCardSerializer(serializers.ModelSerializer):
+ """Article card Serializer"""
+
+ class Meta(object):
+ """meta info"""
+ model = ArticleCard
+ fields = ('id', 'article')
+
diff --git a/junior/tests.py b/junior/tests.py
new file mode 100644
index 0000000..1a75974
--- /dev/null
+++ b/junior/tests.py
@@ -0,0 +1,5 @@
+"""Junior test file"""
+"""Import TestCase"""
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/junior/urls.py b/junior/urls.py
new file mode 100644
index 0000000..4e35d7c
--- /dev/null
+++ b/junior/urls.py
@@ -0,0 +1,68 @@
+""" Urls files"""
+"""Django import"""
+from django.urls import path, include
+from .views import (UpdateJuniorProfile, ValidateGuardianCode, JuniorListAPIView, AddJuniorAPIView,
+ InvitedJuniorAPIView, FilterJuniorAPIView, RemoveJuniorAPIView, JuniorTaskListAPIView,
+ CompleteJuniorTaskAPIView, JuniorPointsListAPIView, ValidateReferralCode,
+ InviteGuardianAPIView, StartTaskAPIView, ReAssignJuniorTaskAPIView, StartArticleAPIView,
+ StartAssessmentAPIView, CheckAnswerAPIView, CompleteArticleAPIView, ReadArticleCardAPIView,
+ CreateArticleCardAPIView, RemoveGuardianCodeAPIView, FAQViewSet, CheckJuniorApiViewSet)
+"""Third party import"""
+from rest_framework import routers
+
+"""Router"""
+router = routers.SimpleRouter()
+# API End points with router
+# in this file
+# we define various api end point
+# that is covered in this guardian
+# section API:- like
+# Create junior profile API, validate junior profile,
+# junior list,
+# add junior list, invited junior,
+# filter-junior,
+# remove junior,
+# junior task list
+"""API End points with router"""
+router.register('create-junior-profile', UpdateJuniorProfile, basename='profile-update')
+# validate guardian code API"""
+router.register('validate-guardian-code', ValidateGuardianCode, basename='validate-guardian-code')
+# junior list API"""
+router.register('junior-list', JuniorListAPIView, basename='junior-list')
+
+router.register('check-junior', CheckJuniorApiViewSet, basename='check-junior')
+# Add junior list API"""
+router.register('add-junior', AddJuniorAPIView, basename='add-junior')
+# Invited junior list API"""
+router.register('invited-junior', InvitedJuniorAPIView, basename='invited-junior')
+# Filter junior list API"""
+router.register('filter-junior', FilterJuniorAPIView, basename='filter-junior')
+# junior's task list API"""
+router.register('junior-task-list', JuniorTaskListAPIView, basename='junior-task-list')
+# junior's task list API"""
+router.register('junior-points', JuniorPointsListAPIView, basename='junior-points')
+# validate referral code API"""
+router.register('validate-referral-code', ValidateReferralCode, basename='validate-referral-code')
+# invite guardian API"""
+router.register('invite-guardian', InviteGuardianAPIView, basename='invite-guardian')
+# start article"""
+router.register('start-article', StartArticleAPIView, basename='start-article')
+# start assessment api"""
+router.register('start-assessment', StartAssessmentAPIView, basename='start-assessment')
+# check answer api"""
+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)),
+ path('api/v1/remove-junior/', RemoveJuniorAPIView.as_view()),
+ path('api/v1/complete-task/', CompleteJuniorTaskAPIView.as_view()),
+ path('api/v1/start-task/', StartTaskAPIView.as_view()),
+ path('api/v1/reassign-task/', ReAssignJuniorTaskAPIView.as_view()),
+ path('api/v1/complete-article/', CompleteArticleAPIView.as_view()),
+ path('api/v1/read-article-card/', ReadArticleCardAPIView.as_view()),
+ path('api/v1/remove-guardian-code-request/', RemoveGuardianCodeAPIView.as_view())
+]
diff --git a/junior/utils.py b/junior/utils.py
new file mode 100644
index 0000000..eac6ac9
--- /dev/null
+++ b/junior/utils.py
@@ -0,0 +1,82 @@
+"""Account utils"""
+"""Import django"""
+from django.conf import settings
+"""Third party Django app"""
+from templated_email import send_templated_mail
+from .models import JuniorPoints
+from base.constants import NUMBER
+from django.db.models import F, Window
+from django.db.models.functions.window import Rank
+# junior notification
+# email for sending email
+# when guardian create junior profile
+# guardian get email when junior send
+# request for approving the profile and
+# being part of the zod bank and access the platform
+# define junior notification email
+# junior approval email
+from celery import shared_task
+@shared_task()
+def junior_notification_email(recipient_email, full_name, email, password):
+ """Notification email"""
+ from_email = settings.EMAIL_FROM_ADDRESS
+ # recipient email"""
+ recipient_list = [recipient_email]
+ # use send template mail for sending email"""
+ send_templated_mail(
+ template_name='junior_notification_email.email',
+ from_email=from_email,
+ recipient_list=recipient_list,
+ context={
+ 'full_name': full_name,
+ 'url':'link',
+ 'email': email,
+ 'password': password
+ }
+ )
+ return full_name
+@shared_task()
+def junior_approval_mail(guardian, full_name):
+ """junior approval mail"""
+ from_email = settings.EMAIL_FROM_ADDRESS
+ recipient_list = [guardian]
+ # use send template mail for sending email"""
+ send_templated_mail(
+ template_name='junior_approval_mail.email',
+ from_email=from_email,
+ recipient_list=recipient_list,
+ context={
+ 'full_name': full_name
+ }
+ )
+ return full_name
+
+def update_positions_based_on_points():
+ """Update position of the junior"""
+ # First, retrieve all the JuniorPoints instances ordered by total_points in descending order.
+ juniors_points = JuniorPoints.objects.order_by('-total_points', 'created_at')
+
+ # Now, iterate through the queryset and update the position field based on the order.
+ position = NUMBER['one']
+ for junior_point in juniors_points:
+ junior_point.position = position
+ junior_point.save()
+ position += NUMBER['one']
+
+
+def get_junior_leaderboard_rank(junior_obj):
+ """
+ to get junior's position/rank
+ :param junior_obj:
+ :return: junior's position/rank
+ """
+ queryset = JuniorPoints.objects.filter(
+ junior__is_verified=True
+ ).select_related('junior', 'junior__auth').annotate(rank=Window(
+ expression=Rank(),
+ order_by=[F('total_points').desc(), 'junior__created_at']
+ )).order_by('-total_points', 'junior__created_at')
+
+ junior = next((query for query in queryset if query.junior == junior_obj), None)
+
+ return junior.rank if junior else None
diff --git a/junior/views.py b/junior/views.py
new file mode 100644
index 0000000..9b1d67e
--- /dev/null
+++ b/junior/views.py
@@ -0,0 +1,852 @@
+"""Junior view file"""
+import os
+
+from rest_framework import viewsets, status, generics,views
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.pagination import PageNumberPagination
+from django.contrib.auth.models import User
+from rest_framework.filters import SearchFilter
+from django.db.models import F
+
+import datetime
+import requests
+
+from rest_framework.viewsets import GenericViewSet, mixins
+import math
+
+from base.pagination import CustomPageNumberPagination
+
+"""Django app import"""
+from drf_yasg.utils import swagger_auto_schema
+from drf_yasg import openapi
+from django.core.management import call_command
+from drf_yasg.views import get_schema_view
+# Import guardian's model,
+# Import junior's model,
+# Import account's model,
+# Import constant from
+# base package,
+# Import messages from
+# base package,
+# Import some functions
+# from utils file
+# Import account's serializer
+# Import account's task
+# import junior serializer
+# Import update_positions_based_on_points
+# Import upload_image_to_alibaba
+# Import custom_response, custom_error_response
+# Import constants
+from django.db.models import Sum
+from junior.models import (Junior, JuniorPoints, JuniorGuardianRelationship, JuniorArticlePoints, JuniorArticle,
+ JuniorArticleCard, FAQ)
+from .serializers import (CreateJuniorSerializer, JuniorDetailListSerializer, AddJuniorSerializer,
+ RemoveJuniorSerializer, CompleteTaskSerializer, JuniorPointsSerializer,
+ AddGuardianSerializer, StartTaskSerializer, ReAssignTaskSerializer,
+ RemoveGuardianCodeSerializer, FAQSerializer, CreateArticleCardSerializer)
+from guardian.models import Guardian, JuniorTask
+from guardian.serializers import TaskDetailsSerializer, TaskDetailsjuniorSerializer
+from base.messages import ERROR_CODE, SUCCESS_CODE
+from base.constants import NUMBER, ARTICLE_STATUS, none, GUARDIAN
+from account.utils import custom_response, custom_error_response, task_status_fun
+from guardian.utils import upload_image_to_alibaba
+from .utils import update_positions_based_on_points
+from notifications.utils import send_notification
+from notifications.constants import REMOVE_JUNIOR, ARTICLE_REWARD_POINTS, ASSOCIATE_EXISTING_JUNIOR
+from web_admin.models import Article, ArticleSurvey, SurveyOption, ArticleCard
+from web_admin.serializers.article_serializer import (ArticleSerializer, ArticleListSerializer,
+ StartAssessmentSerializer)
+""" Define APIs """
+# Define validate guardian code API,
+# update junior profile,
+# list of all assosicated junior
+# Add junior API
+# invite junior API
+# search junior API
+# remove junior API,
+# approve junior API
+# create referral code
+# validation API
+# invite guardian API
+# by junior
+# Start task
+# by junior API
+# Create your views here.
+class UpdateJuniorProfile(viewsets.ModelViewSet):
+ """Update junior profile"""
+ serializer_class = CreateJuniorSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('post',)
+
+ def create(self, request, *args, **kwargs):
+ """Create Junior Profile"""
+ try:
+ request_data = request.data
+ image = request.data.get('image')
+ image_url = ''
+ if image:
+ # check image size
+ if image.size == NUMBER['zero']:
+ return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST)
+ # convert into file
+ filename = f"images/{image.name}"
+ # upload image on ali baba
+ image_url = upload_image_to_alibaba(image, filename)
+ request_data = {"image": image_url}
+ serializer = CreateJuniorSerializer(context={"user":request.user, "image":image_url,
+ "first_name": request.data.get('first_name'),
+ "last_name": request.data.get('last_name')
+ },
+ data=request_data)
+ if serializer.is_valid():
+ """save serializer"""
+ serializer.save()
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ if e.detail:
+ error_detail = e.detail.get('error', None)
+ else:
+ error_detail = str(e)
+ return custom_error_response(error_detail, response_status=status.HTTP_400_BAD_REQUEST)
+
+class ValidateGuardianCode(viewsets.ModelViewSet):
+ """Check guardian code exist or not"""
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+
+ def list(self, request, *args, **kwargs):
+ """check guardian code
+ Params
+ "guardian_code"
+ """
+ try:
+ guardian_code = self.request.GET.get('guardian_code').split(',')
+ for code in guardian_code:
+ # fetch guardian object
+ guardian_data = Guardian.objects.filter(guardian_code=code).exists()
+ if guardian_data:
+ # successfully check guardian code
+ return custom_response(SUCCESS_CODE['3013'], response_status=status.HTTP_200_OK)
+ else:
+ return custom_error_response(ERROR_CODE["2022"], 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 JuniorListAPIView(viewsets.ModelViewSet):
+ """Junior list of assosicated guardian"""
+
+ serializer_class = JuniorDetailListSerializer
+ queryset = Junior.objects.all()
+ permission_classes = [IsAuthenticated]
+ filter_backends = (SearchFilter,)
+ search_fields = ['auth__first_name', 'auth__last_name']
+ http_method_names = ('get',)
+
+ def get_queryset(self):
+ queryset = self.filter_queryset(self.queryset)
+ return queryset
+
+ def list(self, request, *args, **kwargs):
+ """ junior list"""
+ try:
+ # update_positions_based_on_points, function removed
+ guardian_data = Guardian.objects.filter(user__email=request.user).last()
+ # fetch junior object
+ if guardian_data:
+ queryset = self.get_queryset()
+ queryset = queryset.filter(guardian_code__icontains=str(guardian_data.guardian_code))
+ # use JuniorDetailListSerializer serializer
+ serializer = JuniorDetailListSerializer(queryset, context={"guardian_code":
+ guardian_data.guardian_code}, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(ERROR_CODE['2045'], response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class CheckJuniorApiViewSet(viewsets.GenericViewSet):
+ """
+ api to check whether given user exist or not
+ """
+ serializer_class = None
+ permission_classes = [IsAuthenticated]
+
+ def get_queryset(self):
+ junior = Junior.objects.filter(auth__email=self.request.data.get('email')).first()
+ return junior
+
+ def create(self, request, *args, **kwargs):
+ """
+ :param request:
+ :return:
+ """
+ junior = self.get_queryset()
+ data = {
+ 'junior_exist': True if junior else False
+ }
+ return custom_response(None, data)
+
+
+class AddJuniorAPIView(viewsets.ModelViewSet):
+ """Add Junior by guardian"""
+ serializer_class = AddJuniorSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('post',)
+
+ def create(self, request, *args, **kwargs):
+ """ add junior
+ { "gender":"1",
+ "first_name":"abc",
+ "last_name":"xyz",
+ "dob":"2023-12-12",
+ "relationship":"2",
+ "email":"abc@yopmail.com"
+ }"""
+ try:
+ if user := User.objects.filter(username=request.data['email']).first():
+ data = self.associate_guardian(user)
+ if data == none:
+ return custom_error_response(ERROR_CODE['2077'], response_status=status.HTTP_400_BAD_REQUEST)
+ elif not data:
+ return custom_error_response(ERROR_CODE['2076'], response_status=status.HTTP_400_BAD_REQUEST)
+ elif data == "Max":
+ return custom_error_response(ERROR_CODE['2081'], response_status=status.HTTP_400_BAD_REQUEST)
+ return custom_response(SUCCESS_CODE['3021'], response_status=status.HTTP_200_OK)
+
+ info_data = {'user': request.user, 'relationship': str(request.data['relationship']),
+ 'email': request.data['email'], 'first_name': request.data['first_name'],
+ 'last_name': request.data['last_name'], 'image':None}
+ profile_image = request.data.get('image')
+ if profile_image:
+ # check image size
+ if profile_image.size == NUMBER['zero']:
+ return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST)
+ # convert into file
+ filename = f"images/{profile_image.name}"
+ # upload image on ali baba
+ image_url = upload_image_to_alibaba(profile_image, filename)
+ info_data.update({"image": image_url})
+
+ # use AddJuniorSerializer serializer
+ serializer = AddJuniorSerializer(data=request.data, context=info_data)
+ if serializer.is_valid():
+ # save serializer
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3021'], serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+ 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 is None:
+ return none
+ if junior.guardian_code and (guardian.guardian_code in junior.guardian_code):
+ return False
+ if junior.guardian_code and ('-' in junior.guardian_code):
+ junior.guardian_code.remove('-')
+ if not junior.guardian_code:
+ junior.guardian_code = [guardian.guardian_code]
+ elif type(junior.guardian_code) is list and len(junior.guardian_code) < 3:
+ junior.guardian_code.append(guardian.guardian_code)
+ else:
+ return "Max"
+ if junior.guardian_code_status and ('-' in junior.guardian_code_status):
+ junior.guardian_code_status.remove('-')
+ if not junior.guardian_code_status:
+ junior.guardian_code_status = [str(NUMBER['two'])]
+ else:
+ junior.guardian_code_status.append(str(NUMBER['two']))
+ junior.save()
+ jun_data, created = JuniorGuardianRelationship.objects.get_or_create(guardian=guardian, junior=junior)
+ if jun_data:
+ jun_data.relationship = str(self.request.data['relationship'])
+ jun_data.save()
+ send_notification.delay(ASSOCIATE_EXISTING_JUNIOR, guardian.user_id, GUARDIAN, junior.auth_id, {})
+ return True
+
+
+class InvitedJuniorAPIView(viewsets.ModelViewSet):
+ """Invited Junior list of assosicated guardian"""
+
+ serializer_class = JuniorDetailListSerializer
+ permission_classes = [IsAuthenticated]
+ pagination_class = PageNumberPagination
+ http_method_names = ('get',)
+
+ def get_queryset(self):
+ """Get the queryset for the view"""
+ guardian = Guardian.objects.filter(user__email=self.request.user).last()
+ junior_queryset = Junior.objects.filter(guardian_code__icontains=str(guardian.guardian_code),
+ is_invited=True)
+ return junior_queryset
+ def list(self, request, *args, **kwargs):
+ """ Invited Junior list of assosicated guardian
+ No Params"""
+ try:
+ queryset = self.get_queryset()
+ paginator = self.pagination_class()
+ # pagination
+ paginated_queryset = paginator.paginate_queryset(queryset, request)
+ # use JuniorDetailListSerializer serializer
+ serializer = JuniorDetailListSerializer(paginated_queryset, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class FilterJuniorAPIView(viewsets.ModelViewSet):
+ """filter junior profile"""
+ serializer_class = JuniorDetailListSerializer
+ permission_classes = [IsAuthenticated]
+ pagination_class = PageNumberPagination
+ http_method_names = ('get',)
+
+ @swagger_auto_schema(
+ manual_parameters=[
+ # Example of a query parameter
+ openapi.Parameter(
+ 'title',
+ openapi.IN_QUERY,
+ description='title of the name',
+ type=openapi.TYPE_STRING,
+ ),
+ # Add more parameters as needed
+ ]
+ )
+
+ def get_queryset(self):
+ """Get the queryset for the view"""
+ title = self.request.GET.get('title')
+ guardian_data = Guardian.objects.filter(user__email=self.request.user).last()
+ # fetch junior query
+ queryset = Junior.objects.filter(guardian_code__icontains=str(guardian_data.guardian_code),
+ is_invited=True, auth__first_name=title)
+ return queryset
+
+ def list(self, request, *args, **kwargs):
+ """Filter junior"""
+ try:
+ queryset = self.get_queryset()
+ paginator = self.pagination_class()
+ # use Pagination
+ paginated_queryset = paginator.paginate_queryset(queryset, request)
+ # use JuniorDetailListSerializer serializer
+ serializer = JuniorDetailListSerializer(paginated_queryset, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class RemoveJuniorAPIView(views.APIView):
+ """Remove junior API
+ Params
+ id=37"""
+ serializer_class = RemoveJuniorSerializer
+ model = Junior
+ permission_classes = [IsAuthenticated]
+
+ def put(self, request, format=None):
+ try:
+ junior_id = self.request.GET.get('id')
+ guardian = Guardian.objects.filter(user__email=self.request.user).last()
+ # fetch junior query
+ junior_queryset = Junior.objects.filter(id=junior_id,
+ guardian_code__icontains=str(guardian.guardian_code)).last()
+ if junior_queryset:
+ # use RemoveJuniorSerializer serializer
+ serializer = RemoveJuniorSerializer(junior_queryset, context={"guardian_code":guardian.guardian_code},
+ data=request.data, partial=True)
+ if serializer.is_valid():
+ # save serializer
+ serializer.save()
+ JuniorGuardianRelationship.objects.filter(guardian=guardian, junior=junior_queryset).delete()
+ send_notification.delay(REMOVE_JUNIOR, None, GUARDIAN, 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:
+ return custom_error_response(ERROR_CODE['2034'], 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 JuniorTaskListAPIView(viewsets.ModelViewSet):
+ """Junior task list"""
+ serializer_class = TaskDetailsjuniorSerializer
+ permission_classes = [IsAuthenticated]
+ filter_backends = (SearchFilter,)
+ search_fields = ['task_name', ]
+ pagination_class = PageNumberPagination
+ http_method_names = ('get',)
+
+ def get_queryset(self):
+ queryset = JuniorTask.objects.filter(junior__auth=self.request.user
+ ).select_related('junior', 'junior__auth'
+ ).order_by('-created_at')
+
+ queryset = self.filter_queryset(queryset)
+ return queryset
+
+ def list(self, request, *args, **kwargs):
+ """Junior task list
+ status=0
+ search='title'
+ page=1"""
+ try:
+ status_value = self.request.GET.get('status')
+ queryset = self.get_queryset()
+ task_status = task_status_fun(status_value)
+ if status_value:
+ queryset = queryset.filter(task_status__in=task_status)
+ paginator = CustomPageNumberPagination()
+ # use Pagination
+ paginated_queryset = paginator.paginate_queryset(queryset, request)
+ # use TaskDetails juniorSerializer serializer
+ serializer = self.serializer_class(paginated_queryset, many=True)
+ return paginator.get_paginated_response(serializer.data)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class CompleteJuniorTaskAPIView(views.APIView):
+ """Payload
+ task_id
+ image"""
+ serializer_class = CompleteTaskSerializer
+ model = JuniorTask
+ permission_classes = [IsAuthenticated]
+
+ def put(self, request, format=None):
+ try:
+ task_id = self.request.data.get('task_id')
+ image = request.data['image']
+ if image and image.size == NUMBER['zero']:
+ return custom_error_response(ERROR_CODE['2035'], response_status=status.HTTP_400_BAD_REQUEST)
+ # create file
+ filename = f"images/{image.name}"
+ # upload image
+ filename = f"images/{image.name}"
+
+ 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
+ ).select_related('guardian', 'junior').last()
+ if task_queryset:
+ if task_queryset.guardian.guardian_code not in task_queryset.junior.guardian_code:
+ return custom_error_response(ERROR_CODE['2085'], response_status=status.HTTP_400_BAD_REQUEST)
+ elif 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
+ elif task_queryset.task_status in [str(NUMBER['four']), str(NUMBER['five'])]:
+ """Already request send """
+ return custom_error_response(ERROR_CODE['2049'], response_status=status.HTTP_400_BAD_REQUEST)
+ serializer = CompleteTaskSerializer(task_queryset, data={'image': image_url}, partial=True)
+ if serializer.is_valid():
+ # save serializer
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3032'], serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+ else:
+ return custom_error_response(ERROR_CODE['2044'], 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 JuniorPointsListAPIView(viewsets.ModelViewSet):
+ """Junior Points viewset"""
+ serializer_class = JuniorPointsSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+
+ def list(self, request, *args, **kwargs):
+ """Junior Points
+ No Params"""
+ try:
+ # update_positions_based_on_points, function removed
+ queryset = JuniorPoints.objects.filter(junior__auth__email=self.request.user).last()
+ # update position of junior
+ serializer = JuniorPointsSerializer(queryset)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class ValidateReferralCode(viewsets.ModelViewSet):
+ """Check guardian code exist or not"""
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+
+ def get_queryset(self):
+ """Get queryset based on referral_code."""
+ referral_code = self.request.GET.get('referral_code')
+ if referral_code:
+ # search referral code in guardian model
+ guardian_queryset = Guardian.objects.filter(referral_code=referral_code).last()
+ if guardian_queryset:
+ return guardian_queryset
+ else:
+ # search referral code in junior model
+ junior_queryset = Junior.objects.filter(referral_code=referral_code).last()
+ if junior_queryset:
+ return junior_queryset
+ return None
+
+ def list(self, request, *args, **kwargs):
+ """check guardian code"""
+ try:
+ if self.get_queryset():
+ return custom_response(SUCCESS_CODE['3033'], response_status=status.HTTP_200_OK)
+ return custom_error_response(ERROR_CODE["2019"], 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 InviteGuardianAPIView(viewsets.ModelViewSet):
+ """Invite guardian by junior"""
+ serializer_class = AddGuardianSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('post',)
+ def create(self, request, *args, **kwargs):
+ """ add guardian
+ {
+ "first_name":"abc",
+ "last_name":"xyz",
+ "email":"abc@yopmail.com",
+ "relationship":2
+ }"""
+ try:
+ if request.data['email'] == '':
+ return custom_error_response(ERROR_CODE['2062'], response_status=status.HTTP_400_BAD_REQUEST)
+ info = {'user': request.user, 'email': request.data['email'], 'first_name': request.data['first_name'],
+ 'last_name': request.data['last_name'], 'relationship': str(request.data['relationship'])}
+ # use AddJuniorSerializer serializer
+ serializer = AddGuardianSerializer(data=request.data, context=info)
+ if serializer.is_valid():
+ # save serializer
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3034'], serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.error, 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 StartTaskAPIView(views.APIView):
+ """Update junior task API
+ Paylod
+ {
+ "task_id":28
+ }"""
+ serializer_class = StartTaskSerializer
+ model = JuniorTask
+ permission_classes = [IsAuthenticated]
+
+ def put(self, request, format=None):
+ try:
+ task_id = self.request.data.get('task_id')
+ task_queryset = JuniorTask.objects.filter(id=task_id, junior__auth__email=self.request.user).last()
+ print("task_queryset==>",task_queryset)
+ if task_queryset and task_queryset.task_status == str(NUMBER['one']):
+ if task_queryset.guardian.guardian_code not in task_queryset.junior.guardian_code:
+ return custom_error_response(ERROR_CODE['2083'], response_status=status.HTTP_400_BAD_REQUEST)
+ # use StartTaskSerializer serializer
+ serializer = StartTaskSerializer(task_queryset, data=request.data, partial=True)
+ if serializer.is_valid():
+ # save serializer
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3035'], serializer.data, response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+ else:
+ # task in another state
+ return custom_error_response(ERROR_CODE['2060'], 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 ReAssignJuniorTaskAPIView(views.APIView):
+ """Update junior task API
+ Payload
+ {
+ "task_id":34,
+ "due_date":"2023-08-22"
+ }
+ """
+ serializer_class = ReAssignTaskSerializer
+ model = JuniorTask
+ permission_classes = [IsAuthenticated]
+
+ def put(self, request, format=None):
+ try:
+ task_id = self.request.data.get('task_id')
+ task_queryset = JuniorTask.objects.filter(id=task_id, guardian__user__email=self.request.user).last()
+ if task_queryset and task_queryset.task_status == str(NUMBER['six']):
+ # use StartTaskSerializer serializer
+ serializer = ReAssignTaskSerializer(task_queryset, data=request.data, partial=True)
+ if serializer.is_valid():
+ # save serializer
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3036'], response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+ else:
+ # task in another state
+ return custom_error_response(ERROR_CODE['2066'], 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 StartArticleAPIView(viewsets.ModelViewSet):
+ """Start article"""
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('post',)
+
+ def create(self, request, *args, **kwargs):
+ """ Payload
+ {
+ "article_id":"2"
+ }"""
+ try:
+ junior_instance = Junior.objects.filter(auth=self.request.user).last()
+ article_id = request.data.get('article_id')
+ article_data = Article.objects.filter(id=article_id).last()
+ if not JuniorArticle.objects.filter(junior=junior_instance, article=article_data).last():
+ JuniorArticle.objects.create(junior=junior_instance, article=article_data, status=str(NUMBER['two']),
+ current_card_page=NUMBER['zero'], current_que_page=NUMBER['zero'])
+ if article_data:
+ question_query = ArticleSurvey.objects.filter(article=article_id)
+ for question in question_query:
+ if not JuniorArticlePoints.objects.filter(junior=junior_instance,
+ article=article_data,
+ question=question):
+ JuniorArticlePoints.objects.create(junior=junior_instance,
+ article=article_data,
+ question=question)
+ return custom_response(SUCCESS_CODE['3040'], response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+class StartAssessmentAPIView(viewsets.ModelViewSet):
+ """Question answer viewset"""
+ serializer_class = StartAssessmentSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+
+ def get_queryset(self):
+ article_id = self.request.GET.get('article_id')
+ # if referral_code:
+ article = Article.objects.filter(id=article_id, is_deleted=False).prefetch_related(
+ 'article_survey'
+ )
+ return article
+ def list(self, request, *args, **kwargs):
+ """Params
+ article_id
+ """
+
+ try:
+ queryset = self.get_queryset()
+ paginator = self.pagination_class()
+ paginated_queryset = paginator.paginate_queryset(queryset, request)
+ serializer = self.serializer_class(paginated_queryset, context={"user":request.user}, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+class CheckAnswerAPIView(viewsets.ModelViewSet):
+ """Params
+ question_id=1
+ answer_id=1"""
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+
+ def get_queryset(self):
+ question_id = self.request.GET.get('question_id')
+ article = ArticleSurvey.objects.filter(id=question_id).last()
+ return article
+ def list(self, request, *args, **kwargs):
+ """ Params
+ question_id=1
+ answer_id=1
+ """
+
+ try:
+ answer_id = self.request.GET.get('answer_id')
+ current_page = self.request.GET.get('current_page')
+ queryset = self.get_queryset()
+ submit_ans = SurveyOption.objects.filter(id=answer_id).last()
+ junior_article_points = JuniorArticlePoints.objects.filter(junior__auth=self.request.user,
+ question=queryset)
+ if submit_ans.is_answer:
+ junior_article_points.update(submitted_answer=submit_ans, is_attempt=True, is_answer_correct=True)
+ JuniorPoints.objects.filter(junior__auth=self.request.user).update(total_points=
+ F('total_points') + queryset.points)
+ else:
+ junior_article_points.update(submitted_answer=submit_ans, is_attempt=True, earn_points=0,
+ is_answer_correct=False)
+ JuniorArticle.objects.filter(junior__auth=self.request.user,
+ article=queryset.article).update(
+ current_que_page=int(current_page) + NUMBER['one'])
+ return custom_response(None, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+class CompleteArticleAPIView(views.APIView):
+ """Params
+ article_id
+ """
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('put', 'get',)
+ def put(self, request, format=None):
+ try:
+ article_id = self.request.data.get('article_id')
+ JuniorArticle.objects.filter(junior__auth=request.user, article__id=article_id).update(
+ is_completed=True, status=str(NUMBER['three'])
+ )
+ return custom_response(SUCCESS_CODE['3041'], response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+ def get(self, request, *args, **kwargs):
+ """ Params
+ article_id=1"""
+ try:
+ article_id = self.request.GET.get('article_id')
+ total_earn_points = JuniorArticlePoints.objects.filter(junior__auth=request.user,
+ article__id=article_id,
+ is_answer_correct=True).aggregate(
+ total_earn_points=Sum('earn_points'))['total_earn_points']
+ data = {"total_earn_points":total_earn_points}
+ if total_earn_points:
+ send_notification.delay(ARTICLE_REWARD_POINTS, None, GUARDIAN,
+ request.user.id, {'points': total_earn_points})
+ return custom_response(SUCCESS_CODE['3042'], data, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class ReadArticleCardAPIView(views.APIView):
+ """Read article card API"""
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('put',)
+
+ def put(self, request, *args, **kwargs):
+ """ Read article card
+ Payload
+ {"article_id":"1",
+ "article_card":"2",
+ "current_page":"2"}"""
+ try:
+ junior_instance = Junior.objects.filter(auth=self.request.user).last()
+ article = self.request.data.get('article_id')
+ article_card = self.request.data.get('article_card')
+ current_page = self.request.data.get('current_page')
+ JuniorArticleCard.objects.filter(junior=junior_instance,
+ article__id=article,
+ article_card__id=article_card).update(is_read=True)
+ JuniorArticle.objects.filter(junior=junior_instance,
+ article__id=article).update(current_card_page=int(current_page)+NUMBER['one'])
+ return custom_response(SUCCESS_CODE['3043'], response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class CreateArticleCardAPIView(viewsets.ModelViewSet):
+ """Start article"""
+ serializer_class = CreateArticleCardSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('post',)
+
+ def create(self, request, *args, **kwargs):
+ """ create article card
+ Params
+ {"article_id":1}"""
+ try:
+ junior_instance = Junior.objects.filter(auth=self.request.user).last()
+ article_id = request.data.get('article_id')
+ article_data = Article.objects.filter(id=article_id).last()
+ if article_data:
+ article_cards = ArticleCard.objects.filter(article=article_id)
+ for article_card in article_cards:
+ if not JuniorArticleCard.objects.filter(junior=junior_instance,
+ article=article_data,
+ article_card=article_card):
+ JuniorArticleCard.objects.create(junior=junior_instance,
+ article=article_data,
+ article_card=article_card)
+ return custom_response(None, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
+
+class RemoveGuardianCodeAPIView(views.APIView):
+ """Remove guardian code request API
+ Payload
+ {"guardian_code"
+ :"GRD037"
+ }"""
+ serializer_class = RemoveGuardianCodeSerializer
+ permission_classes = [IsAuthenticated]
+
+ def put(self, request, format=None):
+ try:
+ guardian_code = self.request.data.get("guardian_code")
+ guardian_data = Guardian.objects.filter(guardian_code=guardian_code).last()
+ junior_queryset = Junior.objects.filter(auth=self.request.user).last()
+ if junior_queryset:
+ # use RemoveGuardianCodeSerializer serializer
+ serializer = RemoveGuardianCodeSerializer(junior_queryset, context = {"guardian_code":guardian_code},
+ data=request.data, partial=True)
+ if serializer.is_valid():
+ # save serializer
+ serializer.save()
+ JuniorGuardianRelationship.objects.filter(guardian=guardian_data, junior=junior_queryset).delete()
+ return custom_response(SUCCESS_CODE['3044'], response_status=status.HTTP_200_OK)
+ return custom_error_response(serializer.errors, response_status=status.HTTP_400_BAD_REQUEST)
+ else:
+ # task in another state
+ return custom_error_response(ERROR_CODE['2047'], response_status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ error_detail = e.detail.get('error', None)
+ return custom_error_response(error_detail, response_status=status.HTTP_400_BAD_REQUEST)
+
+
+class FAQViewSet(GenericViewSet, mixins.CreateModelMixin,
+ mixins.ListModelMixin):
+ """FAQ view set"""
+
+ serializer_class = FAQSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ['get', 'post']
+
+ def get_queryset(self):
+ return FAQ.objects.filter(status=1).order_by('id')
+
+ def create(self, request, *args, **kwargs):
+ """
+ faq create api method
+ :param request:
+ :param args: question, description
+ :param kwargs:
+ :return: success message
+ """
+ load_fixture = request.query_params.get('load_fixture')
+ if load_fixture:
+ call_command('loaddata', 'fixtures/faq.json')
+ return custom_response(SUCCESS_CODE["3045"], response_status=status.HTTP_200_OK)
+ 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/manage.py b/manage.py
new file mode 100755
index 0000000..81f9c88
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+"""Django import"""
+# Import OS module
+import os
+# Import sys module"""
+import sys
+
+# define all function
+# execute command line
+# Import execute from command line
+# fetch django settings
+
+def main():
+ """Main function"""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'zod_bank.settings')
+ try:
+ """Import execute from command line function"""
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ """Show Exception error"""
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ """execute command line function"""
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/nginx/django.conf b/nginx/django.conf
new file mode 100644
index 0000000..af79e46
--- /dev/null
+++ b/nginx/django.conf
@@ -0,0 +1,24 @@
+upstream web {
+ ip_hash;
+ server web:8000;
+ }
+
+ # portal
+ server {
+ location / {
+ proxy_pass http://web/;
+ proxy_set_header Host $http_host;
+ }
+ listen 8000;
+ client_max_body_size 512M;
+ server_name localhost;
+ proxy_read_timeout 900;
+ proxy_connect_timeout 900;
+ proxy_send_timeout 900;
+ #proxy_set_header Host $http_host;
+
+ location /static {
+ autoindex on;
+ alias /usr/src/app/static/;
+ }
+ }
diff --git a/notifications/__init__.py b/notifications/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/notifications/admin.py b/notifications/admin.py
new file mode 100644
index 0000000..4df035a
--- /dev/null
+++ b/notifications/admin.py
@@ -0,0 +1,14 @@
+"""
+notification admin file
+"""
+from django.contrib import admin
+
+from notifications.models import Notification
+
+
+@admin.register(Notification)
+class NotificationAdmin(admin.ModelAdmin):
+ """Notification Admin"""
+ list_display = ['id', 'notification_type', 'notification_to', 'data', 'is_read']
+ list_filter = ['notification_type']
+ search_fields = ['notification_to']
diff --git a/notifications/apps.py b/notifications/apps.py
new file mode 100644
index 0000000..366a893
--- /dev/null
+++ b/notifications/apps.py
@@ -0,0 +1,13 @@
+"""
+notification app file
+"""
+# django imports
+from django.apps import AppConfig
+
+
+class NotificationsConfig(AppConfig):
+ """
+ notification app config
+ """
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'notifications'
diff --git a/notifications/constants.py b/notifications/constants.py
new file mode 100644
index 0000000..6917327
--- /dev/null
+++ b/notifications/constants.py
@@ -0,0 +1,148 @@
+"""
+notification constants file
+"""
+REGISTRATION = 1
+ASSOCIATE_REQUEST = 3
+ASSOCIATE_REJECTED = 4
+ASSOCIATE_APPROVED = 5
+REFERRAL_POINTS = 6
+ASSOCIATE_JUNIOR = 7
+ASSOCIATE_EXISTING_JUNIOR = 8
+
+TASK_ASSIGNED = 9
+TASK_ACTION = 10
+TASK_REJECTED = 11
+TASK_APPROVED = 12
+PENDING_TASK_EXPIRING = 13
+IN_PROGRESS_TASK_EXPIRING = 14
+TOP_JUNIOR = 15
+
+NEW_ARTICLE_PUBLISHED = 16
+ARTICLE_REWARD_POINTS = 17
+REMOVE_JUNIOR = 18
+
+TEST_NOTIFICATION = 99
+
+# notification dictionary
+NOTIFICATION_DICT = {
+ REGISTRATION: {
+ "notification_type": REGISTRATION,
+ "title": "Successfully registered!",
+ "body": "You have registered successfully. Now login and complete your profile."
+ },
+ # user will receive notification as soon junior
+ # sign up application using their guardian code for association
+ ASSOCIATE_REQUEST: {
+ "notification_type": ASSOCIATE_REQUEST,
+ "title": "Associate request!",
+ "body": "You have request from {from_user} to associate with you."
+ },
+ # Juniors will receive notification when
+ # custodians reject their request for associate
+ ASSOCIATE_REJECTED: {
+ "notification_type": ASSOCIATE_REJECTED,
+ "title": "Associate request rejected!",
+ "body": "Your request to associate has been rejected by {from_user}."
+ },
+ # Juniors will receive notification when
+ # custodians approve their request for associate
+ ASSOCIATE_APPROVED: {
+ "notification_type": ASSOCIATE_APPROVED,
+ "title": "Associate request approved!",
+ "body": "Your request to associate has been approved by {from_user}."
+ },
+ # Juniors will receive Notifications
+ # for every Points earned by referrals
+ REFERRAL_POINTS: {
+ "notification_type": REFERRAL_POINTS,
+ "title": "Earn Referral points!",
+ "body": "You earn 5 points for referral."
+ },
+ # Juniors will receive notification
+ # once any custodians add them in their account
+ ASSOCIATE_JUNIOR: {
+ "notification_type": ASSOCIATE_JUNIOR,
+ "title": "Profile already setup!",
+ "body": "Your guardian has already setup your profile."
+ },
+ ASSOCIATE_EXISTING_JUNIOR: {
+ "notification_type": ASSOCIATE_EXISTING_JUNIOR,
+ "title": "Associated to guardian",
+ "body": "Your are associated to your guardian {from_user}."
+ },
+ # Juniors will receive Notification
+ # for every Task Assign by Custodians
+ TASK_ASSIGNED: {
+ "notification_type": TASK_ASSIGNED,
+ "title": "New task assigned!",
+ "body": "{from_user} has assigned you a new task."
+ },
+ # Guardian will receive notification as soon
+ # as junior send task for approval
+ TASK_ACTION: {
+ "notification_type": TASK_ACTION,
+ "title": "Task completion approval!",
+ "body": "{from_user} completed their task {task_name}."
+ },
+ # Juniors will receive notification as soon
+ # as their task is rejected by custodians
+ TASK_REJECTED: {
+ "notification_type": TASK_REJECTED,
+ "title": "Task completion rejected!",
+ "body": "Your task completion request has been rejected by {from_user}."
+ },
+ # Juniors will receive notification as soon as their task is approved by custodians
+ # and for every Points earned by Task completion
+ TASK_APPROVED: {
+ "notification_type": TASK_APPROVED,
+ "title": "Task completion approved!",
+ "body": "Your task completion request has been approved by {from_user}. "
+ "Also you earned 5 points for successful completion."
+ },
+ # Juniors will receive notification when their task end date about to end
+ PENDING_TASK_EXPIRING: {
+ "notification_type": PENDING_TASK_EXPIRING,
+ "title": "Task expiring soon!",
+ "body": "Your task {task_name} is expiring soon. Please complete it."
+ },
+ # User will receive notification when their assigned task is about to end
+ # and juniors have not performed any action
+ IN_PROGRESS_TASK_EXPIRING: {
+ "notification_type": IN_PROGRESS_TASK_EXPIRING,
+ "title": "Task expiring soon!",
+ "body": "{from_user} didn't take any action on assigned task {task_name} and it's expiring soon. "
+ "Please assist to complete it."
+ },
+ # Juniors will receive Notification
+ # related to Leaderboard progress
+ TOP_JUNIOR: {
+ "notification_type": 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: {
+ "notification_type": NEW_ARTICLE_PUBLISHED,
+ "title": "Time to read!",
+ "body": "A new article has been published."
+ },
+ # Juniors will receive notification when they earn points by reading financial Learning
+ ARTICLE_REWARD_POINTS: {
+ "notification_type": ARTICLE_REWARD_POINTS,
+ "title": "Article reward points!",
+ "body": "You are rewarded with {points} points for reading article and answering questions. "
+ },
+ # Juniors will receive notification as soon as their custodians remove them from account
+ REMOVE_JUNIOR: {
+ "notification_type": REMOVE_JUNIOR,
+ "title": "Disassociate by guardian!",
+ "body": "Your guardian has disassociated you."
+ },
+ # Test notification
+ TEST_NOTIFICATION: {
+ "notification_type": TEST_NOTIFICATION,
+ "title": "Test Notification",
+ "body": "This notification is for testing purpose from {from_user}."
+ }
+}
diff --git a/notifications/migrations/0001_initial.py b/notifications/migrations/0001_initial.py
new file mode 100644
index 0000000..ea73b4a
--- /dev/null
+++ b/notifications/migrations/0001_initial.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.2.2 on 2023-07-19 07:40
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Notification',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('notification_type', models.CharField(blank=True, max_length=50, null=True)),
+ ('data', models.JSONField(blank=True, default=dict, null=True)),
+ ('is_read', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(default=django.utils.timezone.now)),
+ ('notification_from', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='from_notification', to=settings.AUTH_USER_MODEL)),
+ ('notification_to', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_notification', to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/notifications/migrations/0002_notification_updated_at.py b/notifications/migrations/0002_notification_updated_at.py
new file mode 100644
index 0000000..7a075fa
--- /dev/null
+++ b/notifications/migrations/0002_notification_updated_at.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-09-29 07:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('notifications', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='notification',
+ name='updated_at',
+ field=models.DateTimeField(auto_now=True),
+ ),
+ ]
diff --git a/notifications/migrations/__init__.py b/notifications/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/notifications/models.py b/notifications/models.py
new file mode 100644
index 0000000..1fb99e9
--- /dev/null
+++ b/notifications/models.py
@@ -0,0 +1,25 @@
+"""
+notification models file
+"""
+# django imports
+from django.db import models
+from django.contrib.auth import get_user_model
+from django.utils import timezone
+
+USER = get_user_model()
+
+
+class Notification(models.Model):
+ """ used to save the notifications """
+ notification_type = models.CharField(max_length=50, blank=True, null=True)
+ notification_to = models.ForeignKey(USER, related_name='to_notification', on_delete=models.CASCADE)
+ notification_from = models.ForeignKey(USER, related_name='from_notification', on_delete=models.SET_NULL,
+ blank=True, null=True)
+ data = models.JSONField(default=dict, blank=True, null=True)
+ is_read = models.BooleanField(default=False)
+ created_at = models.DateTimeField(default=timezone.now)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ """ string representation """
+ return f"{self.notification_to.id} | {self.notification_to.email}"
diff --git a/notifications/serializers.py b/notifications/serializers.py
new file mode 100644
index 0000000..2c7dfec
--- /dev/null
+++ b/notifications/serializers.py
@@ -0,0 +1,53 @@
+"""
+notification serializer file
+"""
+# third party imports
+from rest_framework import serializers
+
+# local imports
+from notifications.utils import register_fcm_token
+from notifications.models import Notification
+
+
+class RegisterDevice(serializers.Serializer):
+ """
+ used to create and validate register device token
+ """
+ registration_id = serializers.CharField(max_length=250)
+ device_id = serializers.CharField(max_length=250)
+ type = serializers.ChoiceField(choices=["ios", "web", "android"])
+
+ class Meta:
+ """ meta class """
+ fields = ('registration_id', 'type', 'device_id')
+
+ def create(self, validated_data):
+ """ override this method to create device token for users """
+ registration_id = validated_data['registration_id']
+ device_type = validated_data['type']
+ return register_fcm_token(self.context['user_id'], registration_id,
+ validated_data['device_id'], device_type)
+
+
+class NotificationListSerializer(serializers.ModelSerializer):
+ """List of notification"""
+ badge = serializers.SerializerMethodField()
+
+ class Meta(object):
+ """meta info"""
+ model = Notification
+ fields = ['id', 'notification_type', 'data', 'badge', 'is_read', 'updated_at']
+
+ @staticmethod
+ def get_badge(obj):
+ return Notification.objects.filter(notification_to=obj.notification_to, is_read=False).count()
+
+
+class ReadNotificationSerializer(serializers.ModelSerializer):
+ """User task Serializer"""
+ id = serializers.ListSerializer(child=serializers.IntegerField())
+
+ class Meta(object):
+ """Meta class"""
+ model = Notification
+ fields = ('id',)
diff --git a/notifications/tests.py b/notifications/tests.py
new file mode 100644
index 0000000..9cec499
--- /dev/null
+++ b/notifications/tests.py
@@ -0,0 +1,98 @@
+"""
+notification test file
+"""
+# third party imports
+from fcm_django.models import FCMDevice
+
+# django imports
+from django.urls import reverse
+from rest_framework import status
+
+from account.models import UserNotification
+# local imports
+from account.serializers import GuardianSerializer
+from notifications.models import Notification
+from web_admin.tests.test_set_up import AnalyticsSetUp
+
+
+class NotificationTestCase(AnalyticsSetUp):
+ """
+ test notification
+ """
+ def setUp(self) -> None:
+ """
+ test data up
+ :return:
+ """
+ super(NotificationTestCase, self).setUp()
+
+ # notification settings create
+ UserNotification.objects.create(user=self.user)
+
+ # notification create
+ self.notification = Notification.objects.create(notification_to=self.user, notification_from=self.user_3)
+
+ # to get guardian/user auth token
+ self.guardian_data = GuardianSerializer(
+ self.guardian, context={'user_type': 2}
+ ).data
+ self.auth_token = self.guardian_data['auth_token']
+
+ # api header
+ self.header = {
+ 'HTTP_AUTHORIZATION': f'Bearer {self.auth_token}',
+ 'Content-Type': "Application/json"
+ }
+
+ def test_notification_list(self):
+ """
+ test notification list
+ :return:
+ """
+
+ url = reverse('notifications:notifications-list')
+ response = self.client.get(url, **self.header)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming only one notification exists in the database
+ self.assertEqual(Notification.objects.filter(notification_to=self.user).count(), 1)
+
+ def test_fcm_register(self):
+ """
+ test fcm register
+ :return:
+ """
+ url = reverse('notifications:notifications-device')
+ data = {
+ 'registration_id': 'registration_id',
+ 'device_id': 'device_id',
+ 'type': 'ios'
+ }
+ response = self.client.post(url, data, **self.header)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # device created for user
+ self.assertEqual(FCMDevice.objects.count(), 1)
+
+ def test_send_test_notification(self):
+ """
+ test send test notification
+ :return:
+ """
+ url = reverse('notifications:notifications-test')
+ response = self.client.get(url, **self.header)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming one notification exists in the database and two created after api run
+ self.assertEqual(Notification.objects.filter(notification_to=self.user).count(), 3)
+
+ def test_mark_as_read(self):
+ """
+ test mark as read
+ :return:
+ """
+ url = reverse('notifications:notifications-mark-as-read')
+ data = {
+ 'id': [self.notification.id]
+ }
+ response = self.client.patch(url, data, **self.header)
+ self.notification.refresh_from_db()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(self.notification.is_read, True)
diff --git a/notifications/urls.py b/notifications/urls.py
new file mode 100644
index 0000000..b184d02
--- /dev/null
+++ b/notifications/urls.py
@@ -0,0 +1,18 @@
+"""
+notifications urls file
+"""
+# django imports
+from django.urls import path, include
+from rest_framework import routers
+
+# local imports
+from notifications.views import NotificationViewSet
+
+# initiate router
+router = routers.SimpleRouter()
+
+router.register('notifications', NotificationViewSet, basename='notifications')
+
+urlpatterns = [
+ path('api/v1/', include(router.urls)),
+]
diff --git a/notifications/utils.py b/notifications/utils.py
new file mode 100644
index 0000000..9e360df
--- /dev/null
+++ b/notifications/utils.py
@@ -0,0 +1,165 @@
+"""
+notifications utils file
+"""
+# third party imports
+from fcm_django.models import FCMDevice
+from celery import shared_task
+from firebase_admin.messaging import Message, Notification as FirebaseNotification
+
+# django imports
+from django.contrib.auth import get_user_model
+from django.db.models import Q
+
+# local imports
+from account.models import UserNotification
+from account.utils import get_user_full_name
+from base.constants import GUARDIAN, JUNIOR
+from guardian.models import Guardian, JuniorTask
+from junior.models import Junior
+from notifications.constants import NOTIFICATION_DICT
+from notifications.models import Notification
+
+User = get_user_model()
+
+
+def register_fcm_token(user_id, registration_id, device_id, device_type):
+ """ used to register the fcm device token"""
+ FCMDevice.objects.filter(registration_id=registration_id).delete()
+ device, _ = FCMDevice.objects.update_or_create(user_id=user_id,
+ defaults={'device_id': device_id, 'type': device_type,
+ 'active': True,
+ 'registration_id': registration_id})
+ return device
+
+
+def remove_fcm_token(user_id: int, access_token: str, registration_id) -> None:
+ """
+ remove access_token and registration_token
+ """
+ try:
+ # remove fcm token for this device
+ FCMDevice.objects.filter(user_id=user_id).delete()
+ except Exception as e:
+ print(e)
+
+
+def get_from_user_details(from_user_id, from_user_type):
+ """
+ used to get from user details
+ """
+ from_user = None
+ from_user_name = None
+ from_user_image = None
+ if from_user_id:
+ if from_user_type == GUARDIAN:
+ guardian = Guardian.objects.filter(user_id=from_user_id).select_related('user').first()
+ from_user = guardian.user
+ from_user_name = get_user_full_name(from_user)
+ from_user_image = guardian.image
+ elif from_user_type == JUNIOR:
+ junior = Junior.objects.filter(auth_id=from_user_id).select_related('auth').first()
+ from_user = junior.auth
+ from_user_name = get_user_full_name(from_user)
+ from_user_image = junior.image
+ return from_user_name, from_user_image, from_user
+
+
+def get_notification_data(notification_type, from_user_id, from_user_type, to_user_id, extra_data):
+ """
+ get notification and push data
+ :param from_user_type: GUARDIAN or JUNIOR
+ :param notification_type: notification_type
+ :param from_user_id: from_user obj
+ :param to_user_id: to_user obj
+ :param extra_data: any extra data provided
+ :return: notification and push data
+ """
+ push_data = NOTIFICATION_DICT[notification_type].copy()
+ notification_data = push_data.copy()
+ task_name = None
+ points = extra_data.get('points', None)
+ if 'task_id' in extra_data:
+ task = JuniorTask.objects.filter(id=extra_data.get('task_id')).first()
+ task_name = task.task_name
+ extra_data['task_name'] = task_name
+ extra_data['task_image'] = task.image if task.image else task.default_image
+
+ from_user_name, from_user_image, from_user = get_from_user_details(from_user_id, from_user_type)
+
+ push_data['body'] = push_data['body'].format(from_user=from_user_name, task_name=task_name, points=points)
+ notification_data['body'] = notification_data['body'].format(from_user=from_user_name,
+ task_name=task_name, points=points)
+
+ push_data['to_user_type'] = GUARDIAN if from_user_type == JUNIOR else JUNIOR
+ notification_data['to_user_type'] = GUARDIAN if from_user_type == JUNIOR else JUNIOR
+
+ notification_data['from_user'] = from_user_name
+ notification_data['from_user_image'] = from_user_image
+
+
+ notification_data.update(extra_data)
+ to_user = User.objects.filter(id=to_user_id).first()
+ return notification_data, push_data, from_user, to_user
+
+
+@shared_task()
+def send_notification(notification_type, from_user_id, from_user_type, to_user_id, extra_data):
+ """
+ used to send the push for the given notification type
+ """
+ notification_data, push_data, from_user, to_user = get_notification_data(notification_type, from_user_id,
+ from_user_type, to_user_id, extra_data)
+ user_notification_type = UserNotification.objects.filter(user=to_user).first()
+
+ # notification create method changed on 28sep as per changes required
+ task_id = extra_data['task_id'] if 'task_id' in extra_data else None
+ Notification.objects.update_or_create(data__has_key='task_id', data__task_id=task_id,
+ notification_from=from_user, notification_to=to_user,
+ defaults={
+ 'notification_type': notification_type,
+ 'notification_from': from_user,
+ 'notification_to': to_user,
+ 'data': notification_data
+ })
+
+ if user_notification_type and user_notification_type.push_notification:
+ send_push(to_user, push_data)
+
+
+def send_push(user, data):
+ """ used to send push notification to specific user """
+ data['notification_type'] = str(data['notification_type'])
+ user.fcmdevice_set.filter(active=True).send_message(
+ Message(notification=FirebaseNotification(data['title'], data['body']), data=data)
+ )
+
+
+def send_multiple_push(queryset, data):
+ """ used to send same notification to multiple users """
+ data['notification_type'] = str(data['notification_type'])
+ FCMDevice.objects.filter(user__in=queryset, active=True).send_message(
+ Message(notification=FirebaseNotification(data['title'], data['body']), data=data)
+ )
+
+
+@shared_task()
+def send_notification_multiple_user(notification_type, from_user_id, from_user_type,
+ extra_data: dict = {}):
+ """
+ used to send notification to multiple user for the given notification type
+ """
+ to_user_list = User.objects.filter(junior_profile__is_verified=True, is_superuser=False
+ ).exclude(junior_profile__isnull=True, guardian_profile__isnull=True)
+
+ notification_data, push_data, from_user, _ = get_notification_data(notification_type, from_user_id,
+ from_user_type, None, extra_data)
+
+ notification_list = []
+ for user in to_user_list:
+ notification_list.append(Notification(notification_type=notification_type,
+ notification_to=user,
+ 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/notifications/views.py b/notifications/views.py
new file mode 100644
index 0000000..e11a63b
--- /dev/null
+++ b/notifications/views.py
@@ -0,0 +1,85 @@
+"""
+notifications views file
+"""
+# django imports
+from django.db.models import Q
+from rest_framework.decorators import action
+from rest_framework.permissions import IsAuthenticated, AllowAny
+from rest_framework.response import Response
+from rest_framework import viewsets, status, views
+
+# local imports
+from account.utils import custom_response, custom_error_response
+from base.messages import SUCCESS_CODE, ERROR_CODE
+from base.pagination import CustomPageNumberPagination
+from base.tasks import notify_task_expiry, notify_top_junior
+from notifications.constants import TEST_NOTIFICATION
+from notifications.serializers import RegisterDevice, NotificationListSerializer, ReadNotificationSerializer
+from notifications.utils import send_notification
+from notifications.models import Notification
+
+
+class NotificationViewSet(viewsets.GenericViewSet):
+ """
+ used to do the notification actions
+ """
+ serializer_class = NotificationListSerializer
+ permission_classes = [IsAuthenticated, ]
+
+ def list(self, request, *args, **kwargs) -> Response:
+ """
+ to list user's notifications
+ :param request:
+ :return:
+ """
+ queryset = Notification.objects.filter(notification_to_id=request.auth.payload['user_id']
+ ).select_related('notification_to').order_by('-updated_at', '-id')
+ paginator = CustomPageNumberPagination()
+ paginated_queryset = paginator.paginate_queryset(queryset, request)
+ serializer = self.serializer_class(paginated_queryset, many=True)
+ return paginator.get_paginated_response(serializer.data)
+
+ @action(methods=['post'], detail=False, url_path='device', url_name='device', serializer_class=RegisterDevice)
+ def fcm_registration(self, request):
+ """
+ used to save the fcm token
+ """
+ serializer = self.get_serializer_class()(data=request.data,
+ context={'user_id': request.auth.payload['user_id']})
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return custom_response(SUCCESS_CODE["3000"])
+
+ @action(methods=['get'], detail=False, url_path='test', url_name='test')
+ def send_test_notification(self, request):
+ """
+ to test send notification, task expiry, top junior
+ :return:
+ """
+ notify_task_expiry()
+ notify_top_junior()
+ notification_type = request.query_params.get('type', TEST_NOTIFICATION)
+ from_user_type = request.query_params.get('from_user_type')
+ send_notification(int(notification_type), None, from_user_type, request.auth.payload['user_id'],
+ {})
+ if notification_type and request.query_params.get('clear_all'):
+ Notification.objects.filter(notification_type=notification_type).delete()
+ return custom_response(SUCCESS_CODE["3000"])
+
+ @action(methods=['patch'], url_path='mark-as-read', url_name='mark-as-read', detail=False,
+ serializer_class=ReadNotificationSerializer)
+ def mark_as_read(self, request, *args, **kwargs):
+ """
+ notification list
+ """
+
+ if request.data.get('id'):
+ Notification.objects.filter(id__in=request.data.get('id')).update(is_read=True)
+
+ elif request.query_params.get('mark_all'):
+ Notification.objects.filter(notification_to_id=request.auth.payload['user_id']).update(is_read=True)
+
+ elif request.query_params.get('clear_all'):
+ Notification.objects.filter(notification_to_id=request.auth.payload['user_id']).delete()
+
+ return custom_response(SUCCESS_CODE['3039'], response_status=status.HTTP_200_OK)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..1dddf86
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,105 @@
+aliyun-python-sdk-core==2.13.36
+aliyun-python-sdk-dysmsapi==2.1.2
+aliyun-python-sdk-kms==2.16.1
+aliyun-python-sdk-sts==3.1.1
+amqp==5.1.1
+asgiref==3.7.2
+async-timeout==4.0.2
+billiard==4.1.0
+boto3==1.26.157
+botocore==1.29.157
+CacheControl==0.13.1
+cachetools==5.3.1
+celery==5.3.1
+certifi==2023.5.7
+cffi==1.15.1
+channels==4.0.0
+channels-redis==4.1.0
+charset-normalizer==3.1.0
+click==8.1.3
+click-didyoumean==0.3.0
+click-plugins==1.1.1
+click-repl==0.3.0
+coreapi==2.3.3
+coreschema==0.0.4
+crcmod==1.7
+cron-descriptor==1.4.0
+cryptography==41.0.1
+decouple==0.0.7
+Django==4.2.2
+django-celery-beat==2.5.0
+django-celery-results==2.5.1
+django-cors-headers==4.1.0
+django-dotenv==1.4.2
+django-extensions==3.2.3
+django-phonenumber-field==7.1.0
+django-render-block==0.9.2
+django-ses==3.5.0
+django-smtp-ssl==1.0
+django-storages==1.13.2
+django-templated-email==3.0.1
+django-timezone-field==5.1
+djangorestframework==3.14.0
+djangorestframework-simplejwt==5.2.2
+drf-yasg==1.21.6
+fcm-django==2.0.0
+firebase-admin==6.2.0
+google-api-core==2.11.1
+google-api-python-client==2.93.0
+google-auth==2.21.0
+google-auth-httplib2==0.1.0
+google-cloud-core==2.3.3
+google-cloud-firestore==2.11.1
+google-cloud-storage==2.10.0
+google-crc32c==1.5.0
+google-resumable-media==2.5.0
+googleapis-common-protos==1.59.1
+grpcio==1.56.0
+grpcio-status==1.56.0
+gunicorn==20.1.0
+httplib2==0.22.0
+idna==3.4
+inflection==0.5.1
+itypes==1.2.0
+Jinja2==3.1.2
+jmespath==0.10.0
+kombu==5.3.1
+MarkupSafe==2.1.3
+msgpack==1.0.5
+ntplib==0.4.0
+numpy==1.25.1
+oss2==2.18.0
+packaging==23.1
+phonenumbers==8.13.15
+Pillow==9.5.0
+prompt-toolkit==3.0.38
+proto-plus==1.22.3
+protobuf==4.23.4
+psycopg==3.1.9
+pyasn1==0.5.0
+pyasn1-modules==0.3.0
+pycparser==2.21
+pycryptodome==3.18.0
+PyJWT==2.7.0
+pyparsing==3.1.0
+python-crontab==2.7.1
+python-dateutil==2.8.2
+python-dotenv==1.0.0
+pytz==2023.3
+PyYAML==6.0
+redis==4.5.5
+requests==2.31.0
+rsa==4.9
+s3transfer==0.6.1
+six==1.16.0
+sqlparse==0.4.4
+typing_extensions==4.6.3
+tzdata==2023.3
+uritemplate==4.1.1
+urllib3==1.26.16
+vine==5.0.0
+wcwidth==0.2.6
+
+pandas==2.0.3
+XlsxWriter==3.1.2
+coverage==7.3.1
diff --git a/web_admin/__init__.py b/web_admin/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web_admin/admin.py b/web_admin/admin.py
new file mode 100644
index 0000000..29114de
--- /dev/null
+++ b/web_admin/admin.py
@@ -0,0 +1,38 @@
+"""
+web_admin admin file
+"""
+# django imports
+from django.contrib import admin
+
+# local imports
+from web_admin.models import Article, ArticleCard, ArticleSurvey, SurveyOption, DefaultArticleCardImage
+
+
+@admin.register(Article)
+class ArticleAdmin(admin.ModelAdmin):
+ """Article Admin"""
+ list_display = ['id', 'title', 'description', 'is_published', 'is_deleted']
+
+
+@admin.register(ArticleCard)
+class ArticleCardAdmin(admin.ModelAdmin):
+ """Article Card Admin"""
+ list_display = ['id', 'article', 'title', 'description', 'image_url']
+
+
+@admin.register(ArticleSurvey)
+class ArticleSurveyAdmin(admin.ModelAdmin):
+ """Article Survey Admin"""
+ list_display = ['id', 'article', 'question', 'points']
+
+
+@admin.register(SurveyOption)
+class SurveyOptionAdmin(admin.ModelAdmin):
+ """Survey Option Admin"""
+ list_display = ['id', 'survey', 'option', 'is_answer']
+
+
+@admin.register(DefaultArticleCardImage)
+class DefaultArticleCardImagesAdmin(admin.ModelAdmin):
+ """Default Article Card Images Option Admin"""
+ list_display = ['image_name', 'image_url']
diff --git a/web_admin/apps.py b/web_admin/apps.py
new file mode 100644
index 0000000..2842e02
--- /dev/null
+++ b/web_admin/apps.py
@@ -0,0 +1,13 @@
+"""
+web_admin app file
+"""
+# django imports
+from django.apps import AppConfig
+
+
+class WebAdminConfig(AppConfig):
+ """
+ web admin app config
+ """
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'web_admin'
diff --git a/web_admin/migrations/0001_initial.py b/web_admin/migrations/0001_initial.py
new file mode 100644
index 0000000..ad25a50
--- /dev/null
+++ b/web_admin/migrations/0001_initial.py
@@ -0,0 +1,61 @@
+# Generated by Django 4.2.2 on 2023-07-14 13:15
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Article',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=255)),
+ ('description', models.TextField()),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('is_published', models.BooleanField(default=True)),
+ ('is_deleted', models.BooleanField(default=False)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ArticleSurvey',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('question', models.CharField(max_length=255)),
+ ('points', models.IntegerField()),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_survey', to='web_admin.article')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='SurveyOption',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('option', models.CharField(max_length=255)),
+ ('is_answer', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='survey_options', to='web_admin.articlesurvey')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ArticleCard',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=255)),
+ ('description', models.TextField()),
+ ('image', models.ImageField(upload_to='card_images/')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_cards', to='web_admin.article')),
+ ],
+ ),
+ ]
diff --git a/web_admin/migrations/0002_alter_articlecard_image.py b/web_admin/migrations/0002_alter_articlecard_image.py
new file mode 100644
index 0000000..4f438f5
--- /dev/null
+++ b/web_admin/migrations/0002_alter_articlecard_image.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.2 on 2023-07-20 11:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('web_admin', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='articlecard',
+ name='image',
+ field=models.URLField(blank=True, default=None, null=True),
+ ),
+ ]
diff --git a/web_admin/migrations/0003_defaultarticlecardimage_and_more.py b/web_admin/migrations/0003_defaultarticlecardimage_and_more.py
new file mode 100644
index 0000000..bf2f66f
--- /dev/null
+++ b/web_admin/migrations/0003_defaultarticlecardimage_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.2 on 2023-07-24 14:15
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('web_admin', '0002_alter_articlecard_image'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='DefaultArticleCardImage',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('image_name', models.CharField(max_length=20)),
+ ('image_url', models.URLField()),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.RenameField(
+ model_name='articlecard',
+ old_name='image',
+ new_name='image_url',
+ ),
+ ]
diff --git a/web_admin/migrations/0004_alter_surveyoption_survey.py b/web_admin/migrations/0004_alter_surveyoption_survey.py
new file mode 100644
index 0000000..8d28957
--- /dev/null
+++ b/web_admin/migrations/0004_alter_surveyoption_survey.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.2 on 2023-08-01 07:35
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('web_admin', '0003_defaultarticlecardimage_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='surveyoption',
+ name='survey',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='web_admin.articlesurvey'),
+ ),
+ ]
diff --git a/web_admin/migrations/__init__.py b/web_admin/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web_admin/models.py b/web_admin/models.py
new file mode 100644
index 0000000..5dbef97
--- /dev/null
+++ b/web_admin/models.py
@@ -0,0 +1,81 @@
+"""
+web_admin model file
+"""
+# django imports
+from django.db import models
+
+
+class Article(models.Model):
+ """
+ Article model
+ """
+ title = models.CharField(max_length=255)
+ description = models.TextField()
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ is_published = models.BooleanField(default=True)
+ is_deleted = models.BooleanField(default=False)
+
+ def __str__(self):
+ """Return title"""
+ return f'{self.id} | {self.title}'
+
+
+class ArticleCard(models.Model):
+ """
+ Article Card model
+ """
+ article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='article_cards')
+ title = models.CharField(max_length=255)
+ description = models.TextField()
+ image_url = models.URLField(null=True, blank=True, default=None)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ """Return title"""
+ return f'{self.id} | {self.title}'
+
+
+class ArticleSurvey(models.Model):
+ """
+ Article Survey model
+ """
+ article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='article_survey')
+ question = models.CharField(max_length=255)
+ points = models.IntegerField()
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ """Return title"""
+ return f'{self.id} | {self.question}'
+
+
+class SurveyOption(models.Model):
+ """
+ Survey Options model
+ """
+ survey = models.ForeignKey(ArticleSurvey, on_delete=models.CASCADE, related_name='options')
+ option = models.CharField(max_length=255)
+ is_answer = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ """Return title"""
+ return f'{self.id} | {self.option}'
+
+
+class DefaultArticleCardImage(models.Model):
+ """
+ Default images upload in oss bucket
+ """
+ image_name = models.CharField(max_length=20)
+ image_url = models.URLField()
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ """return image_name as an object"""
+ return self.image_name
diff --git a/web_admin/permission.py b/web_admin/permission.py
new file mode 100644
index 0000000..5ecf33a
--- /dev/null
+++ b/web_admin/permission.py
@@ -0,0 +1,26 @@
+"""
+web_admin permission classes
+"""
+# django imports
+from rest_framework import permissions
+
+
+class AdminPermission(permissions.BasePermission):
+ """
+ to check for usertype admin only
+ """
+ def has_permission(self, request, view):
+ """
+ Return True if user_type is admin
+ """
+ if request.user.is_superuser:
+ return True
+ return False
+
+ def has_object_permission(self, request, view, obj):
+ """
+ check for object level permission
+ """
+ if request.user.is_superuser:
+ return True
+ return False
diff --git a/web_admin/serializers/__init__.py b/web_admin/serializers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web_admin/serializers/analytics_serializer.py b/web_admin/serializers/analytics_serializer.py
new file mode 100644
index 0000000..d744674
--- /dev/null
+++ b/web_admin/serializers/analytics_serializer.py
@@ -0,0 +1,146 @@
+"""
+web_admin analytics serializer file
+"""
+# third party imports
+from rest_framework import serializers
+
+# django imports
+from django.contrib.auth import get_user_model
+
+from account.utils import get_user_full_name
+# local imports
+from base.constants import USER_TYPE, JUNIOR
+
+from junior.models import JuniorPoints, Junior
+
+USER = get_user_model()
+
+
+class JuniorLeaderboardSerializer(serializers.ModelSerializer):
+ """
+ junior leaderboard serializer
+ """
+ name = serializers.SerializerMethodField()
+ first_name = serializers.SerializerMethodField()
+ last_name = serializers.SerializerMethodField()
+
+ class Meta:
+ """
+ meta class
+ """
+ model = Junior
+ fields = ('id', 'name', 'first_name', 'last_name', 'is_active', 'image', 'is_deleted')
+
+ @staticmethod
+ def get_name(obj):
+ """
+ :param obj: junior object
+ :return: full name
+ """
+ return get_user_full_name(obj.auth)
+
+ @staticmethod
+ def get_first_name(obj):
+ """
+ :param obj: junior object
+ :return: first name
+ """
+ return obj.auth.first_name
+
+ @staticmethod
+ def get_last_name(obj):
+ """
+ :param obj: junior object
+ :return: last name
+ """
+ return obj.auth.last_name
+
+
+class LeaderboardSerializer(serializers.ModelSerializer):
+ """
+ leaderboard serializer
+ """
+ user_id = serializers.SerializerMethodField()
+ user_type = serializers.SerializerMethodField()
+ junior = JuniorLeaderboardSerializer()
+ rank = serializers.IntegerField()
+
+ class Meta:
+ """
+ meta class
+ """
+ model = JuniorPoints
+ fields = ('user_id', 'user_type', 'total_points', 'rank', 'junior')
+
+ @staticmethod
+ def get_user_id(obj):
+ return obj.junior.auth.id
+
+ @staticmethod
+ def get_user_type(obj):
+ return JUNIOR
+
+
+class UserCSVReportSerializer(serializers.ModelSerializer):
+ """
+ user csv/xls report serializer
+ """
+ name = serializers.SerializerMethodField()
+ phone_number = serializers.SerializerMethodField()
+ user_type = serializers.SerializerMethodField()
+ is_active = serializers.SerializerMethodField()
+ date_joined = serializers.SerializerMethodField()
+
+ class Meta:
+ """
+ meta class
+ """
+ model = USER
+ fields = ('name', 'email', 'phone_number', 'user_type', 'is_active', 'date_joined')
+
+ @staticmethod
+ def get_name(obj):
+ """
+ :param obj: user object
+ :return: full name
+ """
+ return get_user_full_name(obj)
+
+ @staticmethod
+ def get_phone_number(obj):
+ """
+ :param obj: user object
+ :return: user phone number
+ """
+ if profile := (obj.guardian_profile.all().first() or obj.junior_profile.all().first()):
+ return f"+{profile.country_code}{profile.phone}" \
+ if profile.country_code and profile.phone else profile.phone
+
+ @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()
+
+ @staticmethod
+ def get_is_active(obj):
+ """
+ :param obj: user object
+ :return: user type
+ """
+ if profile := (obj.guardian_profile.all().first() or obj.junior_profile.all().first()):
+ return "Active" if profile.is_active else "Inactive"
+
+ @staticmethod
+ def get_date_joined(obj):
+ """
+ :param obj: user obj
+ :return: formatted date
+ """
+ date = obj.date_joined.strftime("%d %b %Y")
+ return date
diff --git a/web_admin/serializers/article_serializer.py b/web_admin/serializers/article_serializer.py
new file mode 100644
index 0000000..4c62973
--- /dev/null
+++ b/web_admin/serializers/article_serializer.py
@@ -0,0 +1,370 @@
+"""
+web_admin serializers file
+"""
+# django imports
+from rest_framework import serializers
+from django.contrib.auth import get_user_model
+
+from base.constants import (ARTICLE_SURVEY_POINTS, MAX_ARTICLE_CARD, MIN_ARTICLE_SURVEY, MAX_ARTICLE_SURVEY, NUMBER,
+ USER_TYPE, ARTICLE_CARD_IMAGE_FOLDER)
+# 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
+
+USER = get_user_model()
+
+
+class ArticleCardSerializer(serializers.ModelSerializer):
+ """
+ Article Card serializer
+ """
+ id = serializers.IntegerField(required=False)
+ image_name = serializers.CharField(required=False)
+ image_url = serializers.CharField(required=False)
+
+ class Meta:
+ """
+ meta class
+ """
+ model = ArticleCard
+ fields = ('id', 'title', 'description', 'image_name', 'image_url')
+
+ def create(self, validated_data):
+ validated_data['image_url'] = get_image_url(validated_data)
+ article = Article.objects.all().first()
+ article_card = ArticleCard.objects.create(article=article, **validated_data)
+ return article_card
+
+
+class SurveyOptionSerializer(serializers.ModelSerializer):
+ """
+ survey option serializer
+ """
+ id = serializers.IntegerField(required=False)
+
+ class Meta:
+ """
+ meta class
+ """
+ model = SurveyOption
+ fields = ('id', 'option', 'is_answer')
+
+
+class ArticleSurveySerializer(serializers.ModelSerializer):
+ """
+ article survey serializer
+ """
+ id = serializers.IntegerField(required=False)
+ options = SurveyOptionSerializer(many=True)
+
+ class Meta:
+ """
+ meta class
+ """
+ model = ArticleSurvey
+ fields = ('id', 'question', 'options')
+
+
+class ArticleSerializer(serializers.ModelSerializer):
+ """
+ serializer for article API
+ """
+ article_cards = ArticleCardSerializer(many=True)
+ article_survey = ArticleSurveySerializer(many=True)
+
+ class Meta:
+ """
+ meta class
+ """
+ model = Article
+ fields = ('id', 'title', 'description', 'is_published', 'article_cards', 'article_survey')
+
+ def validate(self, attrs):
+ """
+ to validate request data
+ :return: validated attrs
+ """
+ article_cards = attrs.get('article_cards', None)
+ article_survey = attrs.get('article_survey', None)
+ if not 0 < len(article_cards) <= int(MAX_ARTICLE_CARD):
+ raise serializers.ValidationError({'details': ERROR_CODE['2039']})
+ if not int(MIN_ARTICLE_SURVEY) <= len(article_survey) <= int(MAX_ARTICLE_SURVEY):
+ raise serializers.ValidationError({'details': ERROR_CODE['2040']})
+ return attrs
+
+ def create(self, validated_data):
+ """
+ to create article.
+ ID in post data dict is for update api.
+ :return: article object
+ """
+ article_cards = validated_data.pop('article_cards')
+ article_survey = validated_data.pop('article_survey')
+
+ article = Article.objects.create(**validated_data)
+
+ for card in article_cards:
+ card = pop_id(card)
+ card['image_url'] = get_image_url(card)
+ ArticleCard.objects.create(article=article, **card)
+
+ for survey in article_survey:
+ survey = pop_id(survey)
+ options = survey.pop('options')
+ survey_obj = ArticleSurvey.objects.create(article=article, points=ARTICLE_SURVEY_POINTS, **survey)
+ for option in options:
+ 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
+ """
+ article_cards = validated_data.pop('article_cards')
+ article_survey = validated_data.pop('article_survey')
+ instance.title = validated_data.get('title', instance.title)
+ instance.description = validated_data.get('description', instance.description)
+ instance.save()
+ prev_card = list(ArticleCard.objects.filter(article=instance).values_list('id', flat=True))
+ # Update or create cards
+ for card_data in article_cards:
+ card_id = card_data.get('id', None)
+ if card_id:
+ prev_card.remove(card_id)
+ card = ArticleCard.objects.get(id=card_id, article=instance)
+ card.title = card_data.get('title', card.title)
+ card.description = card_data.get('description', card.description)
+ card.image_url = get_image_url(card_data)
+ card.save()
+ else:
+ card_data = pop_id(card_data)
+ card_data['image_url'] = get_image_url(card_data)
+ ArticleCard.objects.create(article=instance, **card_data)
+ ArticleCard.objects.filter(id__in=prev_card, article=instance).delete()
+
+ prev_survey = list(ArticleSurvey.objects.filter(article=instance).values_list('id', flat=True))
+ # Update or create survey sections
+ for survey_data in article_survey:
+ survey_id = survey_data.get('id', None)
+ options_data = survey_data.pop('options')
+ if survey_id:
+ prev_survey.remove(survey_id)
+ survey = ArticleSurvey.objects.get(id=survey_id, article=instance)
+ survey.question = survey_data.get('question', survey.question)
+ survey.save()
+ else:
+ survey_data = pop_id(survey_data)
+ survey = ArticleSurvey.objects.create(article=instance, points=ARTICLE_SURVEY_POINTS, **survey_data)
+
+ # Update or create survey options
+ for option_data in options_data:
+ option_id = option_data.get('id', None)
+ if option_id:
+ option = SurveyOption.objects.get(id=option_id, survey=survey)
+ option.option = option_data.get('option', option.option)
+ option.is_answer = option_data.get('is_answer', option.is_answer)
+ option.save()
+ else:
+ option_data = pop_id(option_data)
+ SurveyOption.objects.create(survey=survey, **option_data)
+ ArticleSurvey.objects.filter(id__in=prev_survey, article=instance).delete()
+
+ return instance
+
+
+class ArticleStatusChangeSerializer(serializers.ModelSerializer):
+ """
+ Article status change serializer
+ """
+ class Meta:
+ """
+ meta class
+ """
+ model = Article
+ fields = ('is_published', )
+
+ def update(self, instance, validated_data):
+ """
+ :param instance: article object
+ :param validated_data:
+ :return:
+ """
+ instance.is_published = validated_data['is_published']
+ instance.save()
+ return instance
+
+
+class DefaultArticleCardImageSerializer(serializers.ModelSerializer):
+ """
+ Article Card serializer
+ """
+ image = serializers.FileField(required=False)
+ image_url = serializers.URLField(required=False)
+
+ class Meta:
+ """
+ meta class
+ """
+ model = DefaultArticleCardImage
+ fields = ('image_name', 'image', 'image_url')
+
+ def validate(self, attrs):
+ """
+ to validate data
+ :return: validated data
+ """
+ if 'image' not in attrs and attrs.get('image') is None:
+ raise serializers.ValidationError({'details': ERROR_CODE['2061']})
+ image = attrs.get('image')
+ if image and image.size == NUMBER['zero']:
+ raise serializers.ValidationError(ERROR_CODE['2035'])
+ return attrs
+
+ def create(self, validated_data):
+ """
+ to create and upload image
+ :return: card_image object
+ """
+ validated_data['image_url'] = get_image_url(validated_data)
+
+ card_image = DefaultArticleCardImage.objects.create(**validated_data)
+ return card_image
+
+
+class ArticleListSerializer(serializers.ModelSerializer):
+ """
+ serializer for article API
+ """
+ image = serializers.SerializerMethodField('get_image')
+ total_points = serializers.SerializerMethodField('get_total_points')
+ is_completed = serializers.SerializerMethodField('get_is_completed')
+
+ class Meta(object):
+ """
+ meta class
+ """
+ model = Article
+ fields = ('id', 'title', 'description', 'image', 'total_points', 'is_completed')
+
+ def get_image(self, obj):
+ """article image"""
+ if obj.article_cards.first():
+ return obj.article_cards.first().image_url
+ return None
+
+ def get_total_points(self, obj):
+ """total points of article"""
+ return obj.article_survey.all().count() * NUMBER['five']
+
+ def get_is_completed(self, obj):
+ """complete all question"""
+ context_data = self.context.get('user')
+ junior_article = JuniorArticle.objects.filter(junior__auth=context_data, article=obj).last()
+ if junior_article:
+ return junior_article.is_completed
+ return False
+
+
+class ArticleQuestionSerializer(serializers.ModelSerializer):
+ """
+ article survey serializer
+ """
+ id = serializers.IntegerField(required=False)
+ options = SurveyOptionSerializer(many=True)
+ is_attempt = serializers.SerializerMethodField('get_is_attempt')
+ correct_answer = serializers.SerializerMethodField('get_correct_answer')
+ attempted_answer = serializers.SerializerMethodField('get_attempted_answer')
+
+ def get_is_attempt(self, obj):
+ """attempt question or not"""
+ context_data = self.context.get('user')
+ junior_article_obj = JuniorArticlePoints.objects.filter(junior__auth=context_data, question=obj).last()
+ if junior_article_obj:
+ return junior_article_obj.is_attempt
+ return False
+
+ def get_correct_answer(self, obj):
+ """attempt question or not"""
+ ans_obj = SurveyOption.objects.filter(survey=obj, is_answer=True).last()
+ if ans_obj:
+ return ans_obj.id
+ return None
+
+ def get_attempted_answer(self, obj):
+ """attempt question or not"""
+ context_data = self.context.get('user')
+ junior_article_obj = JuniorArticlePoints.objects.filter(junior__auth=context_data,
+ question=obj).last()
+ if junior_article_obj and junior_article_obj.submitted_answer:
+ return junior_article_obj.submitted_answer.id
+ return None
+
+ class Meta(object):
+ """
+ meta class
+ """
+ model = ArticleSurvey
+ fields = ('id', 'question', 'options', 'points', 'is_attempt', 'correct_answer', 'attempted_answer')
+
+
+class StartAssessmentSerializer(serializers.ModelSerializer):
+ """
+ serializer for article API
+ """
+ article_survey = ArticleQuestionSerializer(many=True)
+ current_page = serializers.SerializerMethodField('get_current_page')
+
+ def get_current_page(self, obj):
+ """current page"""
+ context_data = self.context.get('user')
+ data = JuniorArticle.objects.filter(junior__auth=context_data, article=obj).last()
+ total_count = obj.article_survey.all().count()
+ if data:
+ return data.current_que_page if data.current_que_page < total_count else data.current_que_page - 1
+ return NUMBER['zero']
+
+ class Meta(object):
+ """
+ meta class
+ """
+ model = Article
+ fields = ('article_survey', 'current_page')
+
+
+class ArticleCardlistSerializer(serializers.ModelSerializer):
+ """
+ Article Card serializer
+ """
+ id = serializers.IntegerField(required=False)
+ image_name = serializers.CharField(required=False)
+ image_url = serializers.CharField(required=False)
+ current_page = serializers.SerializerMethodField('get_current_page')
+
+ def get_current_page(self, obj):
+ """current page"""
+ context_data = self.context.get('user')
+ data = JuniorArticle.objects.filter(junior__auth=context_data, article=obj.article).last()
+ total_count = self.context.get('card_count')
+ if data:
+ return data.current_card_page if data.current_card_page < total_count else data.current_card_page - 1
+ return NUMBER['zero']
+
+ class Meta(object):
+ """
+ meta class
+ """
+ model = ArticleCard
+ fields = ('id', 'title', 'description', 'image_name', 'image_url', 'current_page')
diff --git a/web_admin/serializers/auth_serializer.py b/web_admin/serializers/auth_serializer.py
new file mode 100644
index 0000000..bda89bd
--- /dev/null
+++ b/web_admin/serializers/auth_serializer.py
@@ -0,0 +1,136 @@
+"""
+web_admin auth serializers file
+"""
+# python imports
+from datetime import datetime
+
+# django imports
+from rest_framework import serializers
+from django.contrib.auth import get_user_model
+from django.utils import timezone
+
+# local imports
+from account.models import UserEmailOtp
+from base.constants import USER_TYPE
+from base.messages import ERROR_CODE
+from guardian.tasks import generate_otp
+from base.tasks import send_email
+
+USER = get_user_model()
+
+
+class AdminOTPSerializer(serializers.ModelSerializer):
+ """
+ admin forgot password serializer
+ """
+ email = serializers.EmailField()
+
+ class Meta:
+ """
+ meta class
+ """
+ model = USER
+ fields = ('email',)
+
+ def validate(self, attrs):
+ """ used to validate the incoming data """
+ user = USER.objects.filter(email=attrs.get('email')).first()
+ if not user:
+ raise serializers.ValidationError({'details': ERROR_CODE['2004']})
+ elif not user.is_superuser:
+ raise serializers.ValidationError({'details': ERROR_CODE['2063']})
+ attrs.update({'user': user})
+ return attrs
+
+ def create(self, validated_data):
+ """
+ to send otp
+ :return: user_data
+ """
+ email = validated_data['email']
+ verification_code = generate_otp()
+ template = 'email_reset_verification.email'
+ # Send the verification code to the user's email
+ data = {
+ "verification_code": verification_code
+ }
+ send_email.delay([email], template, data)
+
+ expiry = timezone.now() + timezone.timedelta(days=1)
+ user_data, created = UserEmailOtp.objects.update_or_create(email=email,
+ defaults={
+ "otp": verification_code,
+ "expired_at": expiry,
+ "user_type": dict(USER_TYPE).get('3'),
+ })
+
+ return user_data
+
+
+class AdminVerifyOTPSerializer(serializers.Serializer):
+ """
+ admin verify otp serializer
+ """
+ email = serializers.EmailField()
+ otp = serializers.CharField(max_length=6, min_length=6)
+
+ class Meta:
+ """ meta class """
+ fields = ('email', 'otp',)
+
+ def validate(self, attrs):
+ """
+ to validate data
+ :return: validated data
+ """
+ email = attrs.get('email')
+ otp = attrs.get('otp')
+
+ # fetch email otp object of the user
+ user_otp_details = UserEmailOtp.objects.filter(email=email, otp=otp).last()
+ if not user_otp_details:
+ raise serializers.ValidationError({'details': ERROR_CODE['2008']})
+ if user_otp_details.user_type != dict(USER_TYPE).get('3'):
+ raise serializers.ValidationError({'details': ERROR_CODE['2008']})
+ if user_otp_details.expired_at.replace(tzinfo=None) < datetime.utcnow():
+ raise serializers.ValidationError({'details': ERROR_CODE['2029']})
+ user_otp_details.is_verified = True
+ user_otp_details.save()
+ return attrs
+
+
+class AdminCreatePasswordSerializer(serializers.ModelSerializer):
+ """
+ admin create new password serializer
+ """
+ email = serializers.EmailField()
+ new_password = serializers.CharField()
+ confirm_password = serializers.CharField()
+
+ class Meta:
+ """
+ meta class
+ """
+ model = USER
+ fields = ('email', 'new_password', 'confirm_password')
+
+ def validate(self, attrs):
+ """
+ to validate data
+ :return: validated data
+ """
+ email = attrs.get('email')
+ new_password = attrs.get('new_password')
+ confirm_password = attrs.get('confirm_password')
+
+ # matching password
+ if new_password != confirm_password:
+ raise serializers.ValidationError({'details': ERROR_CODE['2065']})
+
+ user_otp_details = UserEmailOtp.objects.filter(email=email, is_verified=True).last()
+ if not user_otp_details:
+ raise serializers.ValidationError({'details': ERROR_CODE['2064']})
+ if user_otp_details.user_type != dict(USER_TYPE).get('3'):
+ raise serializers.ValidationError({'details': ERROR_CODE['2063']})
+ user_otp_details.delete()
+ return attrs
diff --git a/web_admin/serializers/user_management_serializer.py b/web_admin/serializers/user_management_serializer.py
new file mode 100644
index 0000000..713d984
--- /dev/null
+++ b/web_admin/serializers/user_management_serializer.py
@@ -0,0 +1,286 @@
+"""
+web_admin user_management serializers file
+"""
+# django imports
+from rest_framework import serializers
+from django.contrib.auth import get_user_model
+
+from account.utils import get_user_full_name
+from base.constants import USER_TYPE, GUARDIAN, JUNIOR
+# local imports
+from base.messages import ERROR_CODE, SUCCESS_CODE
+from guardian.models import Guardian
+from junior.models import Junior
+
+USER = get_user_model()
+
+
+class UserManagementListSerializer(serializers.ModelSerializer):
+ """
+ user management serializer
+ """
+ name = serializers.SerializerMethodField()
+ country_code = serializers.SerializerMethodField()
+ phone = serializers.SerializerMethodField()
+ user_type = serializers.SerializerMethodField()
+ is_active = serializers.SerializerMethodField()
+
+ class Meta:
+ """
+ meta class
+ """
+ model = USER
+ fields = ('id', 'name', 'email', 'country_code', 'phone', 'user_type', 'is_active')
+
+ @staticmethod
+ def get_name(obj):
+ """
+ :param obj: user object
+ :return: full name
+ """
+ return get_user_full_name(obj)
+
+ @staticmethod
+ def get_country_code(obj):
+ """
+ :param obj: user object
+ :return: user phone number
+ """
+ if profile := obj.guardian_profile.all().first():
+ return profile.country_code if profile.country_code else None
+ elif profile := obj.junior_profile.all().first():
+ return profile.country_code if profile.country_code else None
+
+ @staticmethod
+ def get_phone(obj):
+ """
+ :param obj: user object
+ :return: user phone number
+ """
+ if profile := obj.guardian_profile.all().first():
+ return profile.phone if profile.phone else None
+ elif profile := obj.junior_profile.all().first():
+ return profile.phone if profile.phone else 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')
+ elif obj.junior_profile.all().first():
+ return dict(USER_TYPE).get('1')
+
+ @staticmethod
+ def get_is_active(obj):
+ """
+ :param obj: user object
+ :return: user type
+ """
+ if profile := obj.guardian_profile.all().first():
+ return profile.is_active
+ elif profile := obj.junior_profile.all().first():
+ return profile.is_active
+
+
+class GuardianSerializer(serializers.ModelSerializer):
+ """
+ guardian serializer
+ """
+ name = serializers.SerializerMethodField()
+ first_name = serializers.SerializerMethodField()
+ last_name = serializers.SerializerMethodField()
+ username = serializers.SerializerMethodField()
+ email = serializers.EmailField(required=False)
+
+ class Meta:
+ """
+ meta class
+ """
+ model = Guardian
+ fields = ('id', 'name', 'first_name', 'last_name', 'username', 'dob', 'gender', 'country_code', 'phone',
+ 'is_active', 'country_name', 'image', 'email', 'is_deleted')
+
+ def validate(self, attrs):
+ """
+ to validate request data
+ :return: validated attrs
+ """
+ email = attrs.get('email')
+ phone = attrs.get('phone')
+ if USER.objects.filter(email=email).exclude(id=self.context.get('user_id')).exists():
+ raise serializers.ValidationError({'details': ERROR_CODE['2003']})
+ if Guardian.objects.filter(phone=phone).exclude(user__id=self.context.get('user_id')).exists():
+ raise serializers.ValidationError({'details': ERROR_CODE['2012']})
+ return attrs
+
+ def update(self, instance, validated_data):
+ """
+ to update user and its related profile
+ :param instance: user's guardian object
+ :param validated_data:
+ :return: guardian object
+ """
+ instance.user.email = self.validated_data.get('email', instance.user.email)
+ instance.user.username = self.validated_data.get('email', instance.user.username)
+ instance.user.save()
+ instance.country_code = validated_data.get('country_code', instance.country_code)
+ instance.phone = validated_data.get('phone', instance.phone)
+ instance.save()
+ return instance
+
+ @staticmethod
+ def get_name(obj):
+ """
+ :param obj: guardian object
+ :return: full name
+ """
+ return get_user_full_name(obj.user)
+
+ @staticmethod
+ def get_first_name(obj):
+ """
+ :param obj: guardian object
+ :return: first name
+ """
+ return obj.user.first_name
+
+ @staticmethod
+ def get_last_name(obj):
+ """
+ :param obj: guardian object
+ :return: last name
+ """
+ return obj.user.last_name
+
+ @staticmethod
+ def get_username(obj):
+ """
+ :param obj: guardian object
+ :return: email
+ """
+ return obj.user.username
+
+
+class JuniorSerializer(serializers.ModelSerializer):
+ """
+ junior serializer
+ """
+ name = serializers.SerializerMethodField()
+ first_name = serializers.SerializerMethodField()
+ last_name = serializers.SerializerMethodField()
+ username = serializers.SerializerMethodField()
+ email = serializers.EmailField(required=False)
+
+ class Meta:
+ """
+ meta class
+ """
+ model = Junior
+ fields = ('id', 'name', 'first_name', 'last_name', 'username', 'dob', 'gender', 'country_code', 'phone',
+ 'is_active', 'country_name', 'image', 'email', 'is_deleted')
+
+ def validate(self, attrs):
+ """
+ to validate request data
+ :return: validated attrs
+ """
+ email = attrs.get('email')
+ phone = attrs.get('phone')
+ if email and USER.objects.filter(email=email).exclude(id=self.context.get('user_id')).exists():
+ raise serializers.ValidationError({'details': ERROR_CODE['2003']})
+ if phone and Junior.objects.filter(phone=phone).exclude(auth__id=self.context.get('user_id')).exists():
+ raise serializers.ValidationError({'details': ERROR_CODE['2012']})
+ return attrs
+
+ def update(self, instance, validated_data):
+ """
+ :param instance: user's junior object
+ :param validated_data: validated data
+ :return: instance
+ """
+ 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(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(update_fields=['country_code', 'phone'])
+ return instance
+
+ @staticmethod
+ def get_name(obj):
+ """
+ :param obj: junior object
+ :return: full name
+ """
+ return get_user_full_name(obj.auth)
+
+ @staticmethod
+ def get_first_name(obj):
+ """
+ :param obj: junior object
+ :return: first name
+ """
+ return obj.auth.first_name
+
+ @staticmethod
+ def get_last_name(obj):
+ """
+ :param obj: junior object
+ :return: last name
+ """
+ return obj.auth.last_name
+
+ @staticmethod
+ def get_username(obj):
+ """
+ :param obj: junior object
+ :return: email
+ """
+ return obj.auth.username
+
+
+class UserManagementDetailSerializer(serializers.ModelSerializer):
+ """
+ user management details serializer
+ """
+ user_type = serializers.SerializerMethodField()
+ guardian_profile = GuardianSerializer(many=True)
+ junior_profile = JuniorSerializer(many=True)
+ associated_users = serializers.SerializerMethodField()
+
+ class Meta:
+ """
+ meta class
+ """
+ model = USER
+ fields = ('id', 'user_type', 'email', 'guardian_profile', 'junior_profile', 'associated_users')
+
+ def get_user_type(self, obj):
+ """
+ :param obj: user object
+ :return: user type
+ """
+ return GUARDIAN if self.context['user_type'] == GUARDIAN else JUNIOR
+
+ def get_associated_users(self, obj):
+ """
+ :param obj: user object
+ :return: associated user
+ """
+ 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).select_related('auth')
+ serializer = JuniorSerializer(junior, many=True)
+ return serializer.data
+ 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).select_related('user')
+ serializer = GuardianSerializer(guardian, many=True)
+ return serializer.data
diff --git a/web_admin/tests/__init__.py b/web_admin/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web_admin/tests/test_analytics.py b/web_admin/tests/test_analytics.py
new file mode 100644
index 0000000..02f401e
--- /dev/null
+++ b/web_admin/tests/test_analytics.py
@@ -0,0 +1,109 @@
+"""
+web admin test analytics file
+"""
+# django imports
+from django.urls import reverse
+from rest_framework import status
+
+# local imports
+from web_admin.tests.test_set_up import AnalyticsSetUp
+
+
+class AnalyticsViewSetTestCase(AnalyticsSetUp):
+ """
+ test cases for analytics, users count, new sign-ups,
+ assign tasks report, junior leaderboard, export excel
+ """
+ def setUp(self) -> None:
+ """
+ test data set up
+ :return:
+ """
+ super(AnalyticsViewSetTestCase, self).setUp()
+
+ def test_total_sign_up_count(self):
+ """
+ test total sign up count
+ :return:
+ """
+ self.client.force_authenticate(self.admin_user)
+ url = reverse('web_admin:analytics-users-count')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming four users exists in the database
+ self.assertEqual(response.data['data']['total_users'], 4)
+ # Assuming two guardians exists in the database
+ self.assertEqual(response.data['data']['total_guardians'], 2)
+ # Assuming two juniors exists in the database
+ self.assertEqual(response.data['data']['total_juniors'], 2)
+
+ def test_new_user_sign_ups(self):
+ """
+ test new user sign-ups
+ :return:
+ """
+ self.client.force_authenticate(self.admin_user)
+ url = reverse('web_admin:analytics-new-signups')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming four users exists in the database
+ self.assertEqual(response.data['data'][0]['signups'], 4)
+
+ def test_new_user_sign_ups_between_given_dates(self):
+ """
+ test new user sign-ups
+ :return:
+ """
+ self.client.force_authenticate(self.admin_user)
+ url = reverse('web_admin:analytics-new-signups')
+ query_params = {
+ 'start_date': '2023-09-12',
+ 'end_date': '2023-09-13'
+ }
+ response = self.client.get(url, query_params)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming four users exists in the database
+ self.assertEqual(response.data['data'][0]['signups'], 4)
+
+ def test_assign_tasks_report(self):
+ """
+ test assign tasks report
+ :return:
+ """
+ self.client.force_authenticate(self.admin_user)
+ url = reverse('web_admin:analytics-assign-tasks')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming two completed tasks exists in the database
+ self.assertEqual(response.data['data']['task_completed'], 2)
+ # Assuming two pending tasks exists in the database
+ self.assertEqual(response.data['data']['task_pending'], 2)
+ # Assuming two in progress tasks exists in the database
+ self.assertEqual(response.data['data']['task_in_progress'], 2)
+ # Assuming two requested tasks exists in the database
+ self.assertEqual(response.data['data']['task_requested'], 2)
+ # Assuming two rejected tasks exists in the database
+ self.assertEqual(response.data['data']['task_rejected'], 2)
+ # Assuming two expired tasks exists in the database
+ self.assertEqual(response.data['data']['task_expired'], 2)
+
+ def test_junior_leaderboard(self):
+ """
+ test junior leaderboard
+ :return:
+ """
+ self.client.force_authenticate(self.admin_user)
+ url = reverse('web_admin:analytics-junior-leaderboard')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_export_excel(self):
+ """
+ test export excel
+ :return:
+ """
+ self.client.force_authenticate(self.admin_user)
+ url = reverse('web_admin:analytics-export-excel')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertURLEqual(response.data['data'], self.export_excel_url)
diff --git a/web_admin/tests/test_article.py b/web_admin/tests/test_article.py
new file mode 100644
index 0000000..f77b26b
--- /dev/null
+++ b/web_admin/tests/test_article.py
@@ -0,0 +1,255 @@
+"""
+web_admin test article file
+"""
+# django imports
+from django.test import TestCase
+from django.urls import reverse
+from django.contrib.auth import get_user_model
+from rest_framework.test import APITestCase
+from rest_framework.test import APIClient
+from rest_framework import status
+
+# local imports
+from web_admin.models import Article, ArticleCard, ArticleSurvey, DefaultArticleCardImage
+from web_admin.tests.test_set_up import ArticleTestSetUp
+
+# user model
+User = get_user_model()
+
+
+class ArticleViewSetTestCase(ArticleTestSetUp):
+ """
+ test cases for article create, update, list, retrieve, delete
+ """
+ def setUp(self):
+ """
+ inherit data here
+ :return:
+ """
+ super(ArticleViewSetTestCase, self).setUp()
+
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+
+ def test_article_create_with_default_card_image(self):
+ """
+ test article create with default card_image
+ :return:
+ """
+ url = reverse(self.article_list_url)
+ response = self.client.post(url, self.article_data_with_default_card_image, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Check that a new article was created
+ self.assertEqual(Article.objects.count(), 2)
+
+ def test_article_create_with_base64_card_image(self):
+ """
+ test article create with base64 card image
+ :return:
+ """
+ self.client.force_authenticate(user=self.admin_user)
+ url = reverse(self.article_list_url)
+ response = self.client.post(url, self.article_data_with_base64_card_image, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Check that a new article was created
+ self.assertEqual(Article.objects.count(), 2)
+
+ def test_article_update(self):
+ """
+ test article update
+ :return:
+ """
+ self.client.force_authenticate(user=self.admin_user)
+ url = reverse(self.article_detail_url, kwargs={'pk': self.article.id})
+ response = self.client.put(url, self.article_update_data, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.article.refresh_from_db()
+ self.assertEqual(self.article.title, self.article_update_data['title'])
+ self.assertEqual(self.article.article_cards.count(), 1)
+ self.assertEqual(self.article.article_survey.count(), 6)
+ self.assertEqual(self.article.article_survey.first().options.count(), 3)
+
+ def test_articles_list(self):
+ """
+ test articles list
+ :return:
+ """
+ url = reverse(self.article_list_url)
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming only one article exists in the database
+ self.assertEqual(len(response.data['data']), 1)
+
+ def test_article_retrieve(self):
+ """
+ test article retrieve
+ :return:
+ """
+ url = reverse(self.article_detail_url, kwargs={'pk': self.article.id})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_article_delete(self):
+ """
+ test article delete
+ :return:
+ """
+ url = reverse(self.article_detail_url, kwargs={'pk': self.article.id})
+ response = self.client.delete(url)
+ self.article.refresh_from_db()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(self.article.is_deleted, True)
+
+ def test_article_create_with_invalid_data(self):
+ """
+ test article create with invalid data
+ :return:
+ """
+ url = reverse(self.article_list_url)
+ # Missing article_cards
+ invalid_data = {
+ "title": "Invalid Article",
+ "article_survey": [{"question": "Invalid Survey Question"}]
+ }
+ response = self.client.post(url, invalid_data, format='json')
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_article_status_change(self):
+ """
+ test article status change (publish/un-publish)
+ :return:
+ """
+ url = reverse('web_admin:article-status-change', kwargs={'pk': self.article.id})
+ data = {
+ "is_published": False
+ }
+ response = self.client.patch(url, data, format='json')
+ self.article.refresh_from_db()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(self.article.is_published, False)
+
+ def test_article_card_remove(self):
+ """
+ test article card remove
+ :return:
+ """
+ url = reverse('web_admin:article-remove-card', kwargs={'pk': self.article_card.id})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(ArticleCard.objects.count(), 0)
+
+ def test_article_survey_remove(self):
+ """
+ test article survey remove
+ :return:
+ """
+ url = reverse('web_admin:article-remove-survey', kwargs={'pk': self.article_survey.id})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(ArticleSurvey.objects.count(), 0)
+
+ def test_article_card_create_with_default_card_image(self):
+ """
+ test article card create with default card_image
+ :return:
+ """
+ url = reverse('web_admin:article-test-add-card')
+ response = self.client.post(url, self.article_card_data_with_default_card_image, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Check that a new article card was created
+ self.assertEqual(ArticleCard.objects.count(), 2)
+
+ def test_article_cards_list(self):
+ """
+ test article cards list
+ :return:
+ """
+ url = reverse('web_admin:article-test-list-card')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming only one article exists in the database
+ self.assertEqual(len(response.data['data']), 1)
+
+
+class DefaultArticleCardImagesViewSetTestCase(APITestCase):
+ """
+ test case for default article card image
+ """
+ def setUp(self):
+ """
+ data setup
+ :return:
+ """
+ self.client = APIClient()
+ self.admin_user = User.objects.create_user(username='admin@example.com', email='admin@example.com',
+ password='admin@1234', is_staff=True, is_superuser=True)
+ self.default_image = DefaultArticleCardImage.objects.create(
+ image_name="card1.jpg",
+ image_url="https://example.com/updated_card1.jpg")
+
+ def test_default_article_card_image_list(self):
+ """
+ test default article card image list
+ :return:
+ """
+ self.client.force_authenticate(user=self.admin_user)
+ url = reverse('web_admin:default-card-images-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming only one default article card image exists in the database
+ self.assertEqual(len(response.data['data']), 1)
+
+
+class ArticleListViewSetTestCase(ArticleTestSetUp):
+ """
+ test cases for article list for junior
+ """
+ def setUp(self):
+ """
+ data setup
+ :return:
+ """
+ super(ArticleListViewSetTestCase, self).setUp()
+
+ self.client.force_authenticate(user=self.user)
+
+ def test_article_list(self):
+ """
+ test article list
+ :return:
+ """
+ url = reverse('web_admin:article-list-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming only one article exists in the database
+ self.assertEqual(len(response.data['data']), 1)
+
+
+class ArticleCardListViewSetTestCase(ArticleTestSetUp):
+ """
+ test cases for article card list for junior
+ """
+ def setUp(self):
+ """
+ data setup
+ :return:
+ """
+ super(ArticleCardListViewSetTestCase, self).setUp()
+
+ self.client.force_authenticate(user=self.user)
+
+ def test_article_cards_list(self):
+ """
+ test article cards list for junior
+ :return:
+ """
+ url = reverse('web_admin:article-card-list-list')
+ query_params = {
+ 'article_id': self.article.id,
+ }
+ response = self.client.get(url, query_params)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming only one article exists in the database
+ self.assertEqual(len(response.data['data']), 1)
+
+ # Add more test cases for edge cases, permissions, etc.
diff --git a/web_admin/tests/test_auth.py b/web_admin/tests/test_auth.py
new file mode 100644
index 0000000..fa16ced
--- /dev/null
+++ b/web_admin/tests/test_auth.py
@@ -0,0 +1,160 @@
+"""
+web admin test auth file
+"""
+from datetime import datetime
+from django.utils import timezone
+from django.urls import reverse
+from django.contrib.auth import get_user_model
+
+from rest_framework.test import APITestCase, APIClient
+from rest_framework import status
+
+from account.models import UserEmailOtp
+from base.constants import USER_TYPE
+from guardian.tasks import generate_otp
+from web_admin.tests.test_set_up import BaseSetUp
+
+User = get_user_model()
+
+
+class AdminOTPTestCase(BaseSetUp):
+ """
+ test case to send otp to admin email
+ """
+
+ def setUp(self):
+ """
+ inherit data here
+ :return:
+ """
+ super(AdminOTPTestCase, self).setUp()
+ self.url = reverse('web_admin:admin-otp')
+
+ def test_admin_otp_for_valid_email(self):
+ """
+ test admin otp for valid email
+ :return:
+ """
+ data = {
+ 'email': self.admin_email
+ }
+ response = self.client.post(self.url, data, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(UserEmailOtp.objects.count(), 1)
+
+ def test_admin_otp_for_invalid_email(self):
+ """
+ test admin otp for invalid email
+ :return:
+ """
+ data = {
+ 'email': 'notadmin@example.com'
+ }
+ response = self.client.post(self.url, data, format='json')
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+class AdminVerifyOTPTestCase(BaseSetUp):
+ """
+ test case to verify otp for admin email
+ """
+
+ def setUp(self):
+ """
+ inherit data here
+ :return:
+ """
+ super(AdminVerifyOTPTestCase, self).setUp()
+ self.verification_code = generate_otp()
+ expiry = timezone.now() + timezone.timedelta(days=1)
+ self.user_email_otp = UserEmailOtp.objects.create(email=self.admin_email,
+ otp=self.verification_code,
+ expired_at=expiry,
+ user_type=dict(USER_TYPE).get('3'),
+ )
+ self.url = reverse('web_admin:admin-verify-otp')
+
+ def test_admin_verify_otp_with_valid_otp(self):
+ """
+ test admin verify otp with valid otp
+ :return:
+ """
+
+ data = {
+ 'email': self.admin_email,
+ "otp": self.verification_code
+ }
+
+ response = self.client.post(self.url, data)
+ self.user_email_otp.refresh_from_db()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(self.user_email_otp.is_verified, True)
+
+ def test_admin_verify_otp_with_invalid_otp(self):
+ """
+ test admin verify otp with invalid otp
+ :return:
+ """
+ data = {
+ 'email': self.admin_email,
+ "otp": generate_otp()
+ }
+
+ response = self.client.post(self.url, data)
+ self.user_email_otp.refresh_from_db()
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(self.user_email_otp.is_verified, False)
+
+
+class AdminCreateNewPassword(BaseSetUp):
+ """
+ test case to create new password for admin email
+ """
+
+ def setUp(self):
+ """
+ inherit data here
+ :return:
+ """
+ super(AdminCreateNewPassword, self).setUp()
+ self.verification_code = generate_otp()
+ expiry = timezone.now() + timezone.timedelta(days=1)
+ self.user_email_otp = UserEmailOtp.objects.create(email=self.admin_email,
+ otp=self.verification_code,
+ expired_at=expiry,
+ user_type=dict(USER_TYPE).get('3'),
+ )
+ self.url = reverse('web_admin:admin-create-password')
+
+ def test_admin_create_new_password_after_verification(self):
+ """
+ test admin create new password
+ :return:
+ """
+ self.user_email_otp.is_verified = True
+ self.user_email_otp.save()
+
+ data = {
+ 'email': self.admin_email,
+ "new_password": "New@1234",
+ "confirm_password": "New@1234"
+ }
+
+ response = self.client.post(self.url, data)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(UserEmailOtp.objects.count(), 0)
+
+ def test_admin_create_new_password_without_verification(self):
+ """
+ test admin create new password
+ :return:
+ """
+ data = {
+ 'email': self.admin_email,
+ "new_password": "Some@1234",
+ "confirm_password": "Some@1234"
+ }
+
+ response = self.client.post(self.url, data)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(UserEmailOtp.objects.count(), 1)
diff --git a/web_admin/tests/test_set_up.py b/web_admin/tests/test_set_up.py
new file mode 100644
index 0000000..f8e2e23
--- /dev/null
+++ b/web_admin/tests/test_set_up.py
@@ -0,0 +1,407 @@
+"""
+web_admin test set up file
+"""
+# django imports
+from django.test import TestCase
+from django.contrib.auth import get_user_model
+from django.conf import settings
+from rest_framework.test import APITestCase
+from rest_framework.test import APIClient
+
+# local imports
+from guardian.models import Guardian, JuniorTask
+from junior.models import Junior, JuniorPoints
+from web_admin.models import Article, ArticleCard, ArticleSurvey, SurveyOption
+
+# user model
+User = get_user_model()
+
+# image data in base 64 string
+base64_image = ("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHCBISEhIREhISEhgSERIREREYEhg"
+ "SGRERGRgZGhgYGBgcIS4lHB4rHxgYJjgmLC8xNTU1GiQ7QDszPy40NTEBDAwMEA8QGhISHjEhISE0NDQ0NDQ0N"
+ "DQ0NDQ0NDQxNDQ1NDQxNDQ0NDQ0NDQ0NDExNDE0MTQ0NDQ0NDQ0NDQ0P//AABEIALcBEwMBIgACEQEDEQH/xAAb"
+ "AAACAgMBAAAAAAAAAAAAAAAAAQIEAwUGB//EAEkQAAIBAgMEBgYGBgcIAwAAAAECAAMRBBIhBTFRYQYTIkFxkTJC"
+ "UoGhsRRDYnKCkhYjU8HR4QcVVGOD0vEkM5OissLT8ERVlP/EABgBAQEBAQEAAAAAAAAAAAAAAAABAgME/8QAIBEBA"
+ "QEBAAIDAQEBAQAAAAAAAAERAhIhMUFRAyJhE//aAAwDAQACEQMRAD8AuXiiJivAd4rxQgMGO8hHeQSvCRhKiUcheO"
+ "8CUJG8AZBK8d5G8cBxyMcCV4XkY7wHCKOA4RXheA4RQgOEUJQxHFCFOEUcBiSEhJQJRiREYgStCEIFGEIoBEYXheQ"
+ "AjkYAwJQihAccjHAcIoQHHIxwiQhFAGFSjkbwhEoCKOA4RQgOEUcAjvFKNPaSNdgjlL2VwV7YHeqneOBuL791pRfv"
+ "CVU2jRPrOn3qZP8A0ZpmTE0zuqU/A1FQ/lax+EDJGI8jWvlNuNtPORBhUowZGOBISUgJIQJQihAowvCK8AhFeEgcI"
+ "oQHCKOA7wvFCA4QhAcIrwgShIkxwHHIg90jSqBlzDcb/A2/dAyiCtfUd+4zFiKopo7nciM58ACZDZwtRpA7xSp38c"
+ "ohFmEIQHCKJmABJNgASSdwA3mBT2i5IWkpINS+YjetIemeRNwo5tfukGo303ACwG6wkab2DVn0NSxVTvSmPQXkbEs"
+ "RxYjulGviqjE5dBN5kWLRwR7jInDuOcqpiqi75Zw+0GYgWPlI2gKGU3ChTxXsHzGsyriqg3Van4m6z/rzTZCiGGsq"
+ "vhVJ3wmMa7Sqjvpt96nY/wDIyj4TMu1j61IeK1f+1k/7pWfBHumF8LUFgouWIVBxYmwHn38LwZG+w1ZaiB1DAEstm"
+ "ABBG/UEgzKJFECJTpqbimmUG1s7Elnc82YsffJCRlKEUIFGKEjeAQheK8gcIoxAJKRhAlHIR3gShISUBxyMcDG1TK"
+ "DmUkAbwM1x4DWURj1QqVcVKbNkJvc0mtcA99vHdNlNPtnZocGrTFnGrgfWKONt5kos47FCjUSo3oOOrc+yRcofi05"
+ "zAbYc5KTEj9eq3/uzmBB95kNt4rrKVDtf7tyHXnaynnpce+aYVwTfKFN9XF9bA917A+FpPJHc9KsRkwrjvqMtMeBN"
+ "2/5QZtaAsiDgqj4CcZtrH/SPoaA71zuPtk5B8m852GJxC01zMbDMqDmzEKAPeZZdGe8r4XFCo1TLqqMEze049Kx7w"
+ "NB5zT9INsFf9noHNUqHJcepfTfx+UnRqdXSTCYbtuq5Ge3YVvXYnS537o0b681m1cQMyUd+bt1B/dg6L+JhbwDS67"
+ "inTLOdES7NbuA1NpocPUNSobLnqVGD1ADpRX1VY8QthYbzc981BaqMahu3uEyJhXbcLTZ4fBqgzVDraZmxtNRYWm2"
+ "o1qbKJ3y2tGnTGttJixG0idFEqZKlQ3N98mLrLisffspvMwUsHUPaLHwmxw2BVe7WXGCqLnQCMNalcJV9qWcDS1NQ"
+ "nNkzU6Z4vudx4DsDmXlDE7UarUXD0Dq7ZS+8Io1Zj4AE+6blVVQqILKihUG85Rx4k7zzJk3TqYYkhIiSEjCUIQga6"
+ "8V4ooU4RQkDhFCBKO8jeECV4XkYQJXheRjgOSvIAxwJQvIwgcL0godXUqINASHX7p1+BvNUmthzM67pZhcypU4XRv"
+ "A7vj85ydGmSRyNjOPXrRbwDKlSm7g2QqzAbyVJNvMTNjsbUr1OsqNYJqiA2CcLc+cxV6eU3vpYHz3yuzm2gv47v5z"
+ "E6tRdwjqTmYM3BQcoP3mm42Vj6SEhKb1KjdyWyoL7hY7uJmowOw8RWsxXKvcXOUEclGpnR9VUwtJ3apSRKaliEo2"
+ "JPcAS2pJsNZ15lMU+kONqVGp4Yfq8xV6rXuyi/ZWw0uSL2ufRF9JtdkNTwyZFXmp3kt3knvM4vZuJZ6r16tRFLG7"
+ "FiMxOg7I3AWsL23Cb+njLgEeIuLXnWLG+frKlybgQXA8ZfwTh0Vh3j4ywEm1UqWDAlpKdtwmdUlbHY6nRQsxGm4c"
+ "4VOvXSmpZyBYTjdq7aeqSFuq/OV9qbVesddFvumDZeC+kVUp6hdXqMPVpr6R8ToBzYTn11vqOnPPjNro+i+C6uma"
+ "7DtVhlp/Zog7/AMTDyUcZuryJYdwCgABVG5VAsAOQFoXlkxyt26mDGDIXkgZUTvCLNFCNfETFeRkVK8LyMtYbChwT"
+ "1iLb1b3Y/hkFe8Ly+mzr31bQE+ja9pE4Rcpa7aWvuG+XE2KccylEHteY/hMbFR3N+Yfwg0oXgHS9u18DMtOhnNkLE"
+ "86Zt5i8isUInUqSDvBsfGK8Cd4XkLx3gTvC8xfSKanLULj7qhtPeRJtXo+q5P3iU+SkRpjBtKl1lKonFSR4jUTjMJ"
+ "SHWWPebTuhWTvQtySrTb4Egzkm2biDUY08PXK5mKE0z6N+zecf6y34WD6EHYM25QFVOLXOpm52fsdFIeoAzb1U7l5"
+ "24xbLwddNamFrOfV3Jl56jWdBRwVR99OrT+8EI8w9/hJ/P+d+eisF5xnTPaJd1wqXISz1Ld7n0VPgDf3jhOx26hwlC"
+ "pXcoQgAVbkFnOirqOP755W2Ie7OWOZmLseLHUmd2WajhqisGybt1wrfAmdZsSilU3rVFp2toWy33776i9vA9xM4s1G"
+ "be3mYUUJN8nWAd1jY+UsHrmGx+FT9XTqK9jlOTthT9phoPObLOoF7zy3AdY4ChkRF1NNcxPgb7p0CYmoqdltw3Humt"
+ "Tnqblb/AGnthKYOus4rH7Qaq12OncJDEVyxuxuZXYicuutevnnEGedh0fwfU0QzCz18rvxWn6i/HMfvDhNBsTALWqg"
+ "MLpTHWVeag9lPxNYeGbhOueoWJY7ybmXmfbn/AE6+k80eaYg0YaacmUNJBpiDRhoGa8JjvCBTvIVXyqW4SRMxV0zKR"
+ "5SB4Z6mW5eopOtkcoF8t58Zq9pF+su7FyR6R3kbhfnNwoBRXXdorD2WHcfGa3aydkNwNj4GZ6+G+flSSow3MR7zLaY"
+ "yoPrH/MZrc0yK8SpY2i42p+0b8xjOLf2jKC1JLrZpleXFVCfTbztMqVmZtWJ14zWo8vYLffhrBi4TC8jeF4ErwvI3h"
+ "eBRxnp+4TDeZsV6XuErmYt9tyegZ3ez79VT19RflOCvO+wY/Vp9wfKa5Ss+bnC8LTWdI9pjC4d6gIzt2KQPfUINjbv"
+ "AALHks0w4vpxtIV6woA3p4cnNro1e1m/KDl8S05g0U9kTLpvuTckknUkneSe8kxGFZcMqbsqg/dEvIk1qb78JfpV7/"
+ "wAJZXLufa1RpgHNuPfz8ZYaoRulQVIGrNMe2HFJrfjKz6C8z1nvLmwcKHqGq4ulCzkHc9U+gnPUFjyXnOPU9+nt46s"
+ "52t3s/CfR6S0zo72qVuTkdlPwrp4lpYzSu1Qkkk3JJJPEx5ppzt32sZow0r5pINKjOGkw0wBpNWgZ80Ux5oQJnCniI"
+ "voh9oeU2hpiRKiTK3/lrUwzISyMpuLMhHZccDMWJw9N1ZM4psynsOQtj3Wc6MLzcBRMeJwqOuV1BHxB4g90mU9OAOk"
+ "SvNptvZTUQHBzKTa/eOF5pC8zPXpeloPJq8pB5lR5WV6nqZvsDhxluTa81OysOXbkN/hN/cAWA3Rf+LJPtH6OvtHyh"
+ "1Ce0YZ+UYY8JP8AX61nJjDJ7Rk/oqcWgpPCZkvwjL+p6aTHoFqEC+4b/CUnmw2oP1jeC/ISg4mftpjG+eg4Ydhfuj5"
+ "Tz4DUeInodD0V8B8pvlz6ZQJ5Z0z2t9JxJRDdKGamnBnv228wFH3ec7fpdtY4bDNkNqlW9OlxW47T/hHxKzy6nTCjw"
+ "0m2TQaCNorxGQMGNHsbxWvIQq+tS8iX1tK+Gexliqljy3zU9xicyXRr3AkkgADUknQAc7zucFstaVGnSIBIu9Q39Kq"
+ "3peIAAUchNJ0QwBqVDWYdmibJ9qqRp+UG/iVnZmlymMei9StZ9DT2R5mH0RPZHmZsTT5RdXyjGdigMIvsiAwi+yJf6"
+ "rkfKMUjwPlGGxSGFX2RJfRl4CXOr5GHV8jGGqn0deAhLnVHgfKKMNXDhxxMRoLzjNE85gqU27rzbDMmGB3Bj4awNBR"
+ "vVvKaPGbSqYOotRaqWIAeg5sHW51HA67/AJ7p1Gy9o0cWmemdRbPTPpIeY7xz3RC+mj2slNqNRCrklGygKWOa3Zt77"
+ "TgDsyqfq6g8UInsL4YcpgbDLxXzEl52nk8i/qyt+zf8ss4fZdS+qMPwz1L6MnL5wGHTh8I8WfJzOx8OiIQ6sCSPVO6"
+ "bMYVDqB8xNqKKD/SZkVB/pHgvm0ybPU+qPOZBsq/qjzkdo9H2Japh6jAsSzUmqNkYnUlD6hPDd4b5z1Q1Q3Vv1qNTN"
+ "8pbK6Xtfc1iDbmpjxh5V1C7K5LJjZ9u5ZyVSs4Fn6zLcHODoCO863Q79b219LW0RrVFBDFqikEHXtW7wVuQ48LHkd8"
+ "vjDyq/tjYNarUzJURLCygG1xz01mpbo/i1+soN4syn4CTTDP6dMPUAJtqWsQdbPqQQQdD394tKdTDqxIAWmw9JHBU9"
+ "28W5bxp4zP/AJxrzqwmxMSWAJoXvuFRifLJOtFZlQEqNBr2rDzM4KojKynLlYEZXV7knuytv3A6aHwnQbP6QOoC4he"
+ "sXvqA5WUfaJ0O7vPf6RicyM3q1zvSgYjEYhn6tslNFSnZlcZd7EZTvJ4dwE583Ghnqf8AVNGqDUwrimd7Kq3Qk69un"
+ "3anepF+Jmo2pspDpiafVncKy6o3Dt27Pg4HK8uJrghrHpNxtDo7Up6p2xv0328P9ZpmUqbMCJMaPNEkRMZEBbtZdw9"
+ "6mVFF2YhUHFibASi06/8Ao/2TnqNinHZp3SlzcjtN7gbe88JdxHZbGwlPDUadEAkqLs3tOdWPnLpqLwMnlEXViTI2x"
+ "9YvsmLrF9k+cmaYiyCMgh1q8D5x9aOB848ghkEZE1HrR7J84Gry+MnkEMgjIqHXcvj/AChMmQQjIGzGYmvM5EiymbY"
+ "aTbWyVxFMq2+2jW1BnAK+K2ZWFy+QGyuu9VO+3EcjPV2QzX4/ZtOspSooYH4eElmrKsdGeklPFAU2qIXIuh3dYO8W3"
+ "Zhbd8Jvmp8J4rtnY1bZ9TrKRZ6ZOa2oynxG4852vRHputVVp4huCiqdCp4VP83nxidfVS8/jtDS5SJonhLQPf5GBmm"
+ "FPqTwh1J4S1FArimw3eXGYcbs6nWUCoCCPRcGzIfst+7ce+XYXHEQOLx+yatElizOg+sXeuvrp3feGm+4WUGVaah1q"
+ "hVNiAai9W191jc5O70dOVzeehF14jzE0+L2PSZ+spVFoPmzNaxVj7RUEENzBHO8DlKaI5DremxF75SM6g6HMpKuvO5"
+ "te1wbwq1FtlqqdPRqLVCqDpuLAMh+Hdczpf6opub18QatiCqhxTUaWvckvm78wYGQbZgFwMVTI1AzKrNY9zFXUH8sN"
+ "OQxVGrYgXqLuKsi1CRzUGzjwHumKmhAzKM3fY5l3cGBJB0Oh7+8Tqf0fojdXpr9kKoUeC5tPO3KA2DRvm+k2PENluN"
+ "wBs2tu6+6Qc9QJRr026t11y5hoN18oKmx4g28Zv8ABbfOiYhAL6ZwDY3G7dlbv0uDodDHV2PhdBUxIPeMzrpzUk6Hm"
+ "NZA7OwY/wDmDv8AWo6g9x7OsQWn2PTdc+FqKg9j06RPDJvQ/dIGtyDOe2ps1b5cTT6snRal7o7buxUta+ugYBuU3iU"
+ "sPRXrFx1RFGmc1EZbk20LKQBcgW3cpdp7ewdQij9Io1C4IyZg3WADW62sdATuhl5tj+jVRLtTOcezuPumjdGU2YEHm"
+ "J6i7bNps3+09WBvpB7oh+zmUlfAG3KafpZsFBQfG08W7IqhjTdFdHU2yhSigg6ixN9+8SWNSuIwuHarUSlT1aowVeX"
+ "EnkBc+6e0bLwKUKNOkm5FA8T3k8ydZxf9HOy87VMUV9ECmneATqSPh4C3Gegii3CSNI2ECBJ9SeERonhL6GMgSJAmU"
+ "0WkeobhAx2E5na3S+nRqNSp0alZkYo5DKiq47u8/CdV1DTgekn9H9avXqYinVS9RsxRhaxsBoR4SU9sn6Z4o6jZx/O"
+ "5+VOQPTTGDU7Nb3M/+SaM9DNr0/QZvwYgr8LiMbH24m41z/jB/mTJsXK3P6eYj/62p/xH/wDHCaf6Dtz++80jk0yvV"
+ "TIFhMxSRKTpiMVxItaZssWWBTxNBKilHUMDoRPOukPRl8O5r4fNbeRv04Ed4nqBSQeiGBBAIMzZqy48ewfSerS0/XL"
+ "9lajADwE3CdPrKAaddjxNSdXieimGdixpjXlMQ6JYQfViZ9xc5rmm6ff3NU/4n8pibp4f7O/vqfynVHozhR9WvlD+o"
+ "MMPq0/KI2njy5JunL92G86n8pH9Nqn9nH5iZ2I2Phx9Wn5ZlXZVAfVp5RtM5cOemlfuw6ebfxkW6Y4o7qFPyc/vnoK"
+ "bIo/s18hLC7Io/s18hH+jOXmn6W4zuo0/yt/GH6V439lT/I38Z6imyKPsL5CZP6rpD1F8hHszl5Q3SbHndTQf4ZMP0"
+ "j2kfUT/AIRnqpwNMeovkIhhE9lfyiPaenkWJ2ljapBqUaVQqCFL4ZWsO+2YTD1uL/s9D/8AHT/yT2Q4VPZHkJjxgSl"
+ "TqVOrz9WjPlA1awvYR7PTyX6VtDJ1YpqE3ZBhkC777gvHWYsMMcj56dMI1iMwoICAd+uWdRiemBqXFE06Q3dvD9Yb/"
+ "eFTn7M0+Ix+MqHTH0wDpYBqHxyD5xl/TZ+NZWw+Mdi1SmCSbsxoJr78s6LZeKxlWm+HxFOpiKDBUYIQjUgN1sgt3bmE"
+ "0VXZOIqEE1UxGouoxGcn3m9p1WB6GBaYr0atSjW9JUUE07g6KyvdmG/Um3KWc02fjpMDtzCYemKFOnVQU0zCmaToSt7M"
+ "13tnNyLnXfIVemPsUiebOB8AD84ld8Th8tWmKdanchCCoYi47Bb1XW45ZuU5tcG7OVSx4EkLcd2/v5TWYzrdP0txJOi0"
+ "1HDKT8SZJOl9YelTpt4XX95lGnsGqd5A/C3zIA+Mm+y6VP8A3ldF43dE+AzQjaJ0xHrUj+FwfgQJap9KqB39YnigPyJnN"
+ "ddgU+sap9xGf43t8Jmp45NOqwdd/tELTPwAMnlJ841JXXYba9Kp6FS/4WHzEuAk6g3B1B33E45Fx1Q6YWlTXddv1j28WG"
+ "hnZ4NMtNFIIIUAgm5v36gSSy/C5Z8l2odqZ4SjD2v/AERzLCRGIyBhCbChaEICKwKwhMjGyGYnQwhAr1FMwtTPKEJloivG"
+ "wgjjj8I4TNtVZpgnvlpFMITUZZ1TnApzihKqLU+cQpc4QhEhS5xVsMrqytqGBVhxB3whCOZxHQHAvuDJf2WImtxX9HFK3Y"
+ "rOORsR8oQkxdazE9A8QostdSBuBFvlKI6PY6ibg0z4VCscJi9WK3WzRiwmRqGHY3vndi5Hnebangse2hrpSHBEAjhE6rXj"
+ "GYdEC/arYms/HtkfAS7huhuDXXJmPEkn5whN4xrZ0NkYdPRpIPdLS0kG5QPdCEskKlYcIWHCEJUO3KFuUIQD3QhCB//Z")
+
+# export excel path and
+# export excel url
+export_excel_path = 'analytics/ZOD_Bank_Analytics.xlsx'
+export_excel_url = f"https://{settings.ALIYUN_OSS_BUCKET_NAME}.{settings.ALIYUN_OSS_ENDPOINT}/{export_excel_path}"
+
+
+class BaseSetUp(APITestCase):
+ """
+ basic setup
+ """
+
+ def setUp(self) -> None:
+ """
+ user data
+ :return:
+ """
+ # user and admin email
+ self.user_email = 'user@example.com'
+ self.admin_email = 'admin@example.com'
+ self.client = APIClient()
+
+ # create user
+ self.user = User.objects.create_user(username=self.user_email, email=self.user_email)
+ self.user.set_password('user@1234')
+ self.user.save()
+
+ # create admin
+ self.admin_user = User.objects.create_user(username=self.admin_email, email=self.admin_email,
+ is_staff=True, is_superuser=True)
+ self.admin_user.set_password('admin@1234')
+ self.admin_user.save()
+
+
+class ArticleTestSetUp(BaseSetUp):
+ """
+ test cases data set up
+ for article create, update, list, retrieve and
+ remove card, survey and add test card, list test card and
+ default image upload and list
+ """
+
+ def setUp(self):
+ """
+ set up data for test
+ create user and admin
+ create article, article card and article survey and survey options
+ :return:
+ """
+ super(ArticleTestSetUp, self).setUp()
+
+ # create article
+ self.article = Article.objects.create(title="Existing Article", description="Existing Description",
+ is_published=True)
+ # create article card
+ self.article_card = ArticleCard.objects.create(article=self.article, title="Existing Card 1",
+ description="Existing Card 1 Description")
+ # create article survey
+ self.article_survey = ArticleSurvey.objects.create(article=self.article, points=5,
+ question="Existing Survey Question 1")
+ # create article survey options
+ SurveyOption.objects.create(survey=self.article_survey, option="Existing Option 1", is_answer=True)
+ SurveyOption.objects.create(survey=self.article_survey, option="Existing Option 2", is_answer=False)
+
+ # article api url used for get api
+ self.article_list_url = 'web_admin:article-list'
+
+ # article api url used for post api
+ self.article_detail_url = 'web_admin:article-detail'
+
+ # article card data with default card image
+ self.article_card_data_with_default_card_image = {
+ "title": "Card 1",
+ "description": "Card 1 Description",
+ "image_name": "card1.jpg",
+ "image_url": "https://example.com/card1.jpg"
+ }
+
+ # article card data with base64 image
+ self.article_card_data_with_base64_image = {
+ "title": "Card base64",
+ "description": "Card base64 Description",
+ "image_name": "base64_image.jpg",
+ "image_url": base64_image
+ }
+
+ # article survey option data
+ self.article_survey_option_data = [
+ {"option": "Option 1", "is_answer": True},
+ {"option": "Option 2", "is_answer": False}
+ ]
+
+ # article survey data
+ self.article_survey_data = [
+ {
+ "question": "Survey Question 1",
+ "options": self.article_survey_option_data
+ },
+ {
+ "question": "Survey Question 2",
+ "options": self.article_survey_option_data
+ },
+ {
+ "question": "Survey Question 3",
+ "options": self.article_survey_option_data
+ },
+ {
+ "question": "Survey Question 4",
+ "options": self.article_survey_option_data
+ },
+ {
+ "question": "Survey Question 5",
+ "options": self.article_survey_option_data
+ },
+ ]
+
+ # article data with default card image
+ self.article_data_with_default_card_image = {
+ "title": "Test Article",
+ "description": "Test Description",
+ "article_cards": [
+ self.article_card_data_with_default_card_image
+ ],
+ # minimum 5 article survey needed
+ "article_survey": self.article_survey_data
+ }
+
+ # article data with base64 card image
+ self.article_data_with_base64_card_image = {
+ "title": "Test Article",
+ "description": "Test Description",
+ "article_cards": [
+ self.article_card_data_with_base64_image
+ ],
+ # minimum 5 article survey needed
+ "article_survey": self.article_survey_data
+ }
+
+ # article update data
+ self.article_update_data = {
+ "title": "Updated Article",
+ "description": "Updated Description",
+
+ # updated article card
+ "article_cards": [
+ {
+ "id": self.article_card.id,
+ "title": "Updated Card 1",
+ "description": "Updated Card 1 Description",
+ "image_name": "updated_card1.jpg",
+ "image_url": "https://example.com/updated_card1.jpg"
+ }
+ ],
+ # updated article survey
+ "article_survey": [
+ # updated article survey
+ {
+ "id": self.article_survey.id,
+ "question": "Updated Survey Question 1",
+ "options": [
+ {"id": self.article_survey.options.first().id,
+ "option": "Updated Option 1", "is_answer": False},
+ # New option
+ {"option": "New Option 3", "is_answer": True}
+ ]
+ # added new articles
+ }] + self.article_survey_data
+ }
+
+
+class UserManagementSetUp(BaseSetUp):
+ """
+ test cases for user management
+ users count, new sign-ups,
+ """
+ def setUp(self) -> None:
+ """
+ data setup
+ create new guardian and junior
+ :return:
+ """
+ super(UserManagementSetUp, self).setUp()
+ # guardian codes
+ self.guardian_code_1 = 'GRD123'
+ self.guardian_code_2 = 'GRD456'
+
+ # guardian 1
+ self.guardian = Guardian.objects.create(user=self.user, country_code=91, phone='8765876565',
+ country_name='India', gender=2, is_verified=True,
+ guardian_code=self.guardian_code_1)
+
+ # user 2 email
+ self.user_email_2 = 'user2@yopmail.com'
+ # create user 2
+ self.user_2 = User.objects.create_user(username=self.user_email_2, email=self.user_email_2)
+ self.user_2.set_password('user2@1234')
+ self.user_2.save()
+
+ # guardian 2
+ self.guardian_2 = Guardian.objects.create(user=self.user_2, country_code=92, phone='8765876575',
+ country_name='India', gender=1, is_verified=True,
+ guardian_code=self.guardian_code_2)
+
+ # user 3 email
+ self.user_email_3 = 'user3@yopmail.com'
+ # create user 3
+ self.user_3 = User.objects.create_user(username=self.user_email_3, email=self.user_email_3)
+ self.user_3.set_password('user3@1234')
+ self.user_3.save()
+
+ # junior 1
+ self.junior = Junior.objects.create(auth=self.user_3, country_name='India', gender=2,
+ is_verified=True, guardian_code=[self.guardian_code_1])
+
+ # user 4 email
+ self.user_email_4 = 'user4@yopmail.com'
+ # create user 4
+ self.user_4 = User.objects.create_user(username=self.user_email_4, email=self.user_email_4)
+ self.user_4.set_password('user4@1234')
+ self.user_4.save()
+
+ # junior 2
+ self.junior_2 = Junior.objects.create(auth=self.user_4, country_code=92, phone='8768763443',
+ country_name='India', gender=1, is_verified=True,
+ guardian_code=[self.guardian_code_2])
+
+
+class AnalyticsSetUp(UserManagementSetUp):
+ """
+ test analytics
+ task assign report, junior leaderboard
+ """
+ def setUp(self) -> None:
+ """
+ test data set up
+ create task and assigned to junior
+ create junior points data
+ :return:
+ """
+ super(AnalyticsSetUp, self).setUp()
+
+ # pending tasks 1
+ self.pending_task_1 = JuniorTask.objects.create(guardian=self.guardian, junior=self.junior,
+ task_name='Pending Task 1', task_status=1,
+ due_date='2023-09-12')
+ # pending tasks 2
+ self.pending_task_2 = JuniorTask.objects.create(guardian=self.guardian_2, junior=self.junior_2,
+ task_name='Pending Task 2', task_status=1,
+ due_date='2023-09-12')
+
+ # in progress tasks 1
+ self.in_progress_task_1 = JuniorTask.objects.create(guardian=self.guardian, junior=self.junior,
+ task_name='In progress Task 1', task_status=2,
+ due_date='2023-09-12')
+ # in progress tasks 2
+ self.in_progress_task_2 = JuniorTask.objects.create(guardian=self.guardian_2, junior=self.junior_2,
+ task_name='In progress Task 2', task_status=2,
+ due_date='2023-09-12')
+
+ # rejected tasks 1
+ self.rejected_task_1 = JuniorTask.objects.create(guardian=self.guardian, junior=self.junior,
+ task_name='Rejected Task 1', task_status=3,
+ due_date='2023-09-12')
+ # rejected tasks 2
+ self.rejected_task_2 = JuniorTask.objects.create(guardian=self.guardian_2, junior=self.junior_2,
+ task_name='Rejected Task 2', task_status=3,
+ due_date='2023-09-12')
+
+ # requested task 1
+ self.requested_task_1 = JuniorTask.objects.create(guardian=self.guardian, junior=self.junior,
+ task_name='Requested Task 1', task_status=4,
+ due_date='2023-09-12')
+ # requested task 2
+ self.requested_task_2 = JuniorTask.objects.create(guardian=self.guardian_2, junior=self.junior_2,
+ task_name='Requested Task 2', task_status=4,
+ due_date='2023-09-12')
+
+ # completed task 1
+ self.completed_task_1 = JuniorTask.objects.create(guardian=self.guardian, junior=self.junior,
+ task_name='Completed Task 1', task_status=5,
+ due_date='2023-09-12')
+ # completed task 2
+ self.completed_task_2 = JuniorTask.objects.create(guardian=self.guardian_2, junior=self.junior_2,
+ task_name='Completed Task 2', task_status=5,
+ due_date='2023-09-12')
+
+ # expired task 1
+ self.expired_task_1 = JuniorTask.objects.create(guardian=self.guardian, junior=self.junior,
+ task_name='Expired Task 1', task_status=6,
+ due_date='2023-09-11')
+ # expired task 2
+ self.expired_task_2 = JuniorTask.objects.create(guardian=self.guardian_2, junior=self.junior_2,
+ task_name='Expired Task 2', task_status=6,
+ due_date='2023-09-11')
+
+ # junior point table data
+ JuniorPoints.objects.create(junior=self.junior_2, total_points=50)
+ JuniorPoints.objects.create(junior=self.junior, total_points=40)
+
+ # export excel url
+ self.export_excel_url = export_excel_url
diff --git a/web_admin/tests/test_user_management.py b/web_admin/tests/test_user_management.py
new file mode 100644
index 0000000..c2e7adb
--- /dev/null
+++ b/web_admin/tests/test_user_management.py
@@ -0,0 +1,255 @@
+"""
+web admin test user management file
+"""
+# django imports
+from django.contrib.auth import get_user_model
+from rest_framework import status
+
+# local imports
+from base.constants import GUARDIAN, JUNIOR
+from web_admin.tests.test_set_up import UserManagementSetUp
+
+# user model
+User = get_user_model()
+
+
+class UserManagementViewSetTestCase(UserManagementSetUp):
+ """
+ test cases for user management
+ """
+ def setUp(self) -> None:
+ super(UserManagementViewSetTestCase, self).setUp()
+
+ self.update_data = {
+ 'email': 'user5@yopmail.com',
+ 'country_code': 93,
+ 'phone': '8765454235'
+ }
+ self.user_management_endpoint = "/api/v1/user-management"
+
+ def test_user_management_list_all_users(self):
+ """
+ test user management list all users
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/"
+ response = self.client.get(url, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming four user exists in the database
+ self.assertEqual(len(response.data['data']), 4)
+
+ def test_user_management_list_guardians(self):
+ """
+ test user management list guardians
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/?user_type={GUARDIAN}"
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming two guardians exists in the database
+ self.assertEqual(len(response.data['data']), 2)
+
+ def test_user_management_list_juniors(self):
+ """
+ test user management list juniors
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/?user_type={JUNIOR}"
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Assuming two juniors exists in the database
+ self.assertEqual(len(response.data['data']), 2)
+
+ def test_user_management_list_with_unauthorised_user(self):
+ """
+ test user management list with unauthorised user
+ :return:
+ """
+ # user unauthorised access
+ self.client.force_authenticate(user=self.user)
+ url = f"{self.user_management_endpoint}/"
+ response = self.client.get(url, format='json')
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_user_management_retrieve_guardian(self):
+ """
+ test user management retrieve guardian
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user.id}/?user_type={GUARDIAN}"
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data['data']), 1)
+
+ def test_user_management_retrieve_junior(self):
+ """
+ test user management retrieve junior
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user_3.id}/?user_type={JUNIOR}"
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data['data']), 1)
+
+ def test_user_management_retrieve_without_user_type(self):
+ """
+ test user management retrieve without user type
+ user status is mandatory
+ API will throw error
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user.id}/"
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_user_management_update_guardian(self):
+ """
+ test user management update guardian
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user.id}/?user_type={GUARDIAN}"
+ response = self.client.patch(url, self.update_data, format='json',)
+ self.user.refresh_from_db()
+ self.guardian.refresh_from_db()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(self.user.email, self.update_data['email'])
+ self.assertEqual(self.guardian.country_code, self.update_data['country_code'])
+ self.assertEqual(self.guardian.phone, self.update_data['phone'])
+
+ def test_user_management_update_guardian_with_existing_email(self):
+ """
+ test user management update guardian with existing email
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user.id}/?user_type={GUARDIAN}"
+ data = {
+ 'email': self.user_email_2
+ }
+ response = self.client.patch(url, data, format='json',)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_user_management_update_guardian_with_existing_phone(self):
+ """
+ test user management update guardian with existing phone
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user.id}/?user_type={GUARDIAN}"
+ data = {
+ 'phone': self.guardian_2.phone
+ }
+ response = self.client.patch(url, data, format='json',)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_user_management_update_junior(self):
+ """
+ test user management update junior
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user_3.id}/?user_type={JUNIOR}"
+ response = self.client.patch(url, self.update_data, format='json',)
+ self.user_3.refresh_from_db()
+ self.junior.refresh_from_db()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(self.user_3.email, self.update_data['email'])
+ self.assertEqual(self.junior.country_code, self.update_data['country_code'])
+ self.assertEqual(self.junior.phone, self.update_data['phone'])
+
+ def test_user_management_update_junior_with_existing_email(self):
+ """
+ test user management update guardian with existing phone
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user_3.id}/?user_type={JUNIOR}"
+ data = {
+ 'email': self.user_email_4
+ }
+ response = self.client.patch(url, data, format='json',)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_user_management_update_junior_with_existing_phone(self):
+ """
+ test user management update junior with existing phone
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user_3.id}/?user_type={JUNIOR}"
+ data = {
+ 'phone': self.junior_2.phone
+ }
+ response = self.client.patch(url, data, format='json',)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_user_management_update_without_user_type(self):
+ """
+ test user management update without user type
+ user status is mandatory
+ API will throw error
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user_3.id}/"
+ response = self.client.patch(url, self.update_data, format='json',)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_user_management_change_status_guardian(self):
+ """
+ test user management change status guardian
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user.id}/change-status/?user_type={GUARDIAN}"
+ response = self.client.get(url)
+ self.guardian.refresh_from_db()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(self.guardian.is_active, False)
+
+ def test_user_management_change_status_junior(self):
+ """
+ test user management change status junior
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user_3.id}/change-status/?user_type={JUNIOR}"
+ response = self.client.get(url)
+ self.junior.refresh_from_db()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(self.junior.is_active, False)
+
+ def test_user_management_change_status_without_user_type(self):
+ """
+ test user management change status without user type
+ user status is mandatory
+ API will throw error
+ :return:
+ """
+ # admin user authentication
+ self.client.force_authenticate(user=self.admin_user)
+ url = f"{self.user_management_endpoint}/{self.user.id}/"
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
diff --git a/web_admin/urls.py b/web_admin/urls.py
new file mode 100644
index 0000000..4b0f480
--- /dev/null
+++ b/web_admin/urls.py
@@ -0,0 +1,31 @@
+"""
+web_admin urls file
+"""
+# django imports
+from django.urls import path, include
+from rest_framework import routers
+
+# local imports
+from web_admin.views.analytics import AnalyticsViewSet
+from web_admin.views.article import (ArticleViewSet, DefaultArticleCardImagesViewSet, ArticleListViewSet,
+ ArticleCardListViewSet)
+from web_admin.views.auth import ForgotAndResetPasswordViewSet
+from web_admin.views.user_management import UserManagementViewSet
+
+# initiate router
+router = routers.SimpleRouter()
+
+router.register('article', ArticleViewSet, basename='article')
+router.register('default-card-images', DefaultArticleCardImagesViewSet, basename='default-card-images')
+router.register('user-management', UserManagementViewSet, basename='user')
+router.register('analytics', AnalyticsViewSet, basename='analytics')
+
+router.register('article-list', ArticleListViewSet, basename='article-list')
+router.register('article-card-list', ArticleCardListViewSet, basename='article-card-list')
+
+# forgot and reset password api for admin
+router.register('admin', ForgotAndResetPasswordViewSet, basename='admin')
+
+urlpatterns = [
+ path('api/v1/', include(router.urls)),
+]
diff --git a/web_admin/utils.py b/web_admin/utils.py
new file mode 100644
index 0000000..3dbb3b2
--- /dev/null
+++ b/web_admin/utils.py
@@ -0,0 +1,61 @@
+"""
+web_utils file
+"""
+import base64
+import datetime
+
+from base.constants import ARTICLE_CARD_IMAGE_FOLDER, DATE_FORMAT
+from guardian.utils import upload_image_to_alibaba, upload_base64_image_to_alibaba
+
+
+def pop_id(data):
+ """
+ to pop id, not in use
+ :param data:
+ :return: data
+ """
+ data.pop('id') if 'id' in data else data
+ return data
+
+
+def get_image_url(data):
+ """
+ to get image url
+ :param data:
+ :return: image url
+ """
+ if 'image_url' in data and 'http' in data['image_url']:
+ if 'image_name' in data:
+ data.pop('image_name')
+ 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 = 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_base64_image_to_alibaba(base64_image, filename)
+ return image_url
+ elif 'image' in data and data['image'] is not None:
+ image = data.pop('image')
+ filename = f"{ARTICLE_CARD_IMAGE_FOLDER}/{image.name}"
+ # upload image on ali baba
+ image_url = upload_image_to_alibaba(image, filename)
+ return image_url
+
+
+def get_dates(start_date, end_date):
+ """
+ to get start and end date
+ :param start_date: format (yyyy-mm-dd)
+ :param end_date: format (yyyy-mm-dd)
+ :return: start and end date
+ """
+
+ if start_date and end_date:
+ start_date = datetime.datetime.strptime(start_date, DATE_FORMAT).date()
+ end_date = datetime.datetime.strptime(end_date, DATE_FORMAT).date()
+ else:
+ end_date = datetime.date.today()
+ start_date = end_date - datetime.timedelta(days=6)
+
+ return start_date, end_date
diff --git a/web_admin/views/__init__.py b/web_admin/views/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web_admin/views/analytics.py b/web_admin/views/analytics.py
new file mode 100644
index 0000000..b4f228f
--- /dev/null
+++ b/web_admin/views/analytics.py
@@ -0,0 +1,247 @@
+"""
+web_admin analytics view file
+"""
+# python imports
+import datetime
+import io
+import pandas as pd
+import xlsxwriter
+
+# third party imports
+from rest_framework.viewsets import GenericViewSet
+from rest_framework.decorators import action
+from rest_framework.permissions import IsAuthenticated
+
+# django imports
+from django.contrib.auth import get_user_model
+from django.db.models import Q
+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, get_user_full_name
+from base.constants import PENDING, IN_PROGRESS, REJECTED, REQUESTED, COMPLETED, EXPIRED, TASK_STATUS
+from guardian.models import JuniorTask
+from guardian.utils import upload_excel_file_to_alibaba
+from junior.models import JuniorPoints
+from base.pagination import CustomPageNumberPagination
+from web_admin.permission import AdminPermission
+from web_admin.serializers.analytics_serializer import LeaderboardSerializer, UserCSVReportSerializer
+from web_admin.utils import get_dates
+
+USER = get_user_model()
+
+
+class AnalyticsViewSet(GenericViewSet):
+ """
+ analytics api view
+ to get user report (active users, guardians and juniors counts)
+ to get new user sign up report
+ to get task report (completed, in-progress, requested and rejected tasks count)
+ to get junior leaderboard and ranking
+ """
+ serializer_class = None
+ permission_classes = [IsAuthenticated, AdminPermission]
+
+ def get_queryset(self):
+ user_qs = USER.objects.filter(
+ (Q(junior_profile__is_verified=True) | Q(guardian_profile__is_verified=True)),
+ is_superuser=False
+ ).prefetch_related('guardian_profile',
+ 'junior_profile'
+ ).exclude(junior_profile__isnull=True,
+ guardian_profile__isnull=True).order_by('-date_joined')
+ return user_qs
+
+ @action(methods=['get'], url_name='users-count', url_path='users-count', detail=False)
+ def total_users_count(self, request, *args, **kwargs):
+ """
+ api method to get total users, guardians and juniors
+ :param request: start_date: date format (yyyy-mm-dd)
+ :param request: end_date: date format (yyyy-mm-dd)
+ :return:
+ """
+ start_date, end_date = get_dates(request.query_params.get('start_date'),
+ request.query_params.get('end_date'))
+
+ user_qs = self.get_queryset()
+ queryset = user_qs.filter(date_joined__range=(start_date, (end_date + datetime.timedelta(days=1))))
+
+ data = {'total_users': queryset.count(),
+ 'total_guardians': queryset.filter(junior_profile__isnull=True).count(),
+ 'total_juniors': queryset.filter(guardian_profile__isnull=True).count()}
+
+ return custom_response(None, data)
+
+ @action(methods=['get'], url_name='new-signups', url_path='new-signups', detail=False)
+ def new_signups(self, request, *args, **kwargs):
+ """
+ api method to get new signups
+ :param request: start_date: date format (yyyy-mm-dd)
+ :param request: end_date: date format (yyyy-mm-dd)
+ :return:
+ """
+ start_date, end_date = get_dates(request.query_params.get('start_date'),
+ request.query_params.get('end_date'))
+
+ user_qs = self.get_queryset()
+ signup_data = user_qs.filter(date_joined__range=[start_date, (end_date + datetime.timedelta(days=1))]
+ ).annotate(date=TruncDate('date_joined')
+ ).values('date').annotate(signups=Count('id')).order_by('date')
+
+ return custom_response(None, signup_data)
+
+ @action(methods=['get'], url_name='assign-tasks', url_path='assign-tasks', detail=False)
+ def assign_tasks_report(self, request, *args, **kwargs):
+ """
+ api method to get assign tasks count for (completed, in-progress, requested and rejected) task
+ :param request: start_date: date format (yyyy-mm-dd)
+ :param request: end_date: date format (yyyy-mm-dd)
+ :return:
+ """
+ start_date, end_date = get_dates(request.query_params.get('start_date'),
+ request.query_params.get('end_date'))
+
+ assign_tasks = JuniorTask.objects.filter(
+ created_at__range=[start_date, (end_date + datetime.timedelta(days=1))]
+ )
+
+ data = {
+ 'task_completed': assign_tasks.filter(task_status=COMPLETED).count(),
+ 'task_pending': assign_tasks.filter(task_status=PENDING).count(),
+ 'task_in_progress': assign_tasks.filter(task_status=IN_PROGRESS).count(),
+ 'task_requested': assign_tasks.filter(task_status=REQUESTED).count(),
+ 'task_rejected': assign_tasks.filter(task_status=REJECTED).count(),
+ 'task_expired': assign_tasks.filter(task_status=EXPIRED).count(),
+ }
+
+ return custom_response(None, data)
+
+ @action(methods=['get'], url_name='junior-leaderboard', url_path='junior-leaderboard', detail=False,
+ serializer_class=LeaderboardSerializer)
+ def junior_leaderboard(self, request):
+ """
+ to get junior leaderboard and rank
+ :param request:
+ :return:
+ """
+ queryset = JuniorPoints.objects.filter(
+ junior__is_verified=True
+ ).select_related('junior', 'junior__auth').annotate(rank=Window(
+ expression=Rank(),
+ order_by=[F('total_points').desc(), 'junior__created_at']
+ )).order_by('-total_points', 'junior__created_at')
+ paginator = CustomPageNumberPagination()
+ paginated_queryset = paginator.paginate_queryset(queryset, request)
+ serializer = self.serializer_class(paginated_queryset, many=True)
+ return custom_response(None, serializer.data)
+
+ @action(methods=['get'], url_name='export-excel', url_path='export-excel', detail=False)
+ def export_excel(self, request):
+ """
+ to export users count, task details and top juniors in csv/excel file
+ :param request: start_date: date format (yyyy-mm-dd)
+ :param request: end_date: date format (yyyy-mm-dd)
+ :return:
+ """
+
+ response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ response['Content-Disposition'] = 'attachment; filename="ZOD_Bank_Analytics.xlsx"'
+
+ start_date, end_date = get_dates(request.query_params.get('start_date'),
+ request.query_params.get('end_date'))
+
+ # Use BytesIO for binary data
+ buffer = io.BytesIO()
+
+ # Create an XlsxWriter Workbook object
+ workbook = xlsxwriter.Workbook(buffer)
+
+ # Add sheets
+ sheets = ['Users', 'Assign Tasks', 'Juniors Leaderboard']
+
+ for sheet_name in sheets:
+ worksheet = workbook.add_worksheet(name=sheet_name)
+
+ # sheet 1 for Total Users
+ if sheet_name == 'Users':
+ user_qs = self.get_queryset()
+ queryset = user_qs.filter(date_joined__range=(start_date, (end_date + datetime.timedelta(days=1))))
+ serializer = UserCSVReportSerializer(queryset, many=True)
+
+ df_users = pd.DataFrame([
+ {'Name': user['name'], 'Email': user['email'],
+ 'Phone Number': user['phone_number'], 'User Type': user['user_type'],
+ 'Status': user['is_active'], 'Date Joined': user['date_joined']}
+ for user in serializer.data
+ ])
+ write_excel_worksheet(worksheet, df_users)
+
+ # sheet 2 for Assign Task
+ elif sheet_name == 'Assign Tasks':
+ assign_tasks = JuniorTask.objects.filter(
+ created_at__range=[start_date, (end_date + datetime.timedelta(days=1))]
+ ).select_related('junior__auth', 'guardian__user').only('task_name', 'task_status',
+ 'junior__auth__first_name',
+ 'junior__auth__last_name',
+ 'guardian__user__first_name',
+ 'guardian__user__last_name',)
+
+ df_tasks = pd.DataFrame([
+ {'Task Name': task.task_name, 'Assign To': get_user_full_name(task.junior.auth),
+ 'Assign By': get_user_full_name(task.guardian.user),
+ 'Task Status': dict(TASK_STATUS).get(task.task_status).capitalize()}
+ for task in assign_tasks
+ ])
+
+ write_excel_worksheet(worksheet, df_tasks)
+
+ # sheet 3 for Juniors Leaderboard and rank
+ elif sheet_name == 'Juniors Leaderboard':
+ queryset = JuniorPoints.objects.filter(
+ junior__is_verified=True
+ ).select_related('junior', 'junior__auth').annotate(rank=Window(
+ expression=Rank(),
+ order_by=[F('total_points').desc(), 'junior__created_at']
+ )).order_by('-total_points', 'junior__created_at')[:15]
+ df_leaderboard = pd.DataFrame([
+ {
+ 'Name': get_user_full_name(junior.junior.auth),
+ 'Points': junior.total_points,
+ 'Rank': junior.rank
+ }
+ for junior in queryset
+ ])
+
+ write_excel_worksheet(worksheet, df_leaderboard)
+
+ # Close the workbook to save the content
+ workbook.close()
+
+ # Reset the buffer position and write the content to the response
+ buffer.seek(0)
+ response.write(buffer.getvalue())
+ buffer.close()
+
+ filename = f"{'analytics'}/{'ZOD_Bank_Analytics.xlsx'}"
+ file_link = upload_excel_file_to_alibaba(response, filename)
+ return custom_response(None, file_link)
+
+
+def write_excel_worksheet(worksheet, dataframe):
+ """
+ to perform write action on worksheets
+ :param worksheet:
+ :param dataframe:
+ :return: worksheet
+ """
+ for idx, col in enumerate(dataframe.columns):
+ # Write header
+ worksheet.write(0, idx, col)
+ for row_num, row in enumerate(dataframe.values, start=1):
+ for col_num, value in enumerate(row):
+ worksheet.write(row_num, col_num, value)
+ return worksheet
diff --git a/web_admin/views/article.py b/web_admin/views/article.py
new file mode 100644
index 0000000..1aa46a9
--- /dev/null
+++ b/web_admin/views/article.py
@@ -0,0 +1,286 @@
+"""
+web_admin views file
+"""
+# django imports
+from rest_framework.viewsets import GenericViewSet, mixins
+from rest_framework.filters import OrderingFilter, SearchFilter
+from rest_framework import status, viewsets
+from rest_framework.decorators import action
+from rest_framework.permissions import IsAuthenticated, AllowAny
+from django.contrib.auth import get_user_model
+
+# local imports
+from account.utils import custom_response, custom_error_response
+from base.constants import USER_TYPE
+from base.messages import SUCCESS_CODE, ERROR_CODE
+from web_admin.models import Article, ArticleCard, ArticleSurvey, DefaultArticleCardImage
+from web_admin.permission import AdminPermission
+from web_admin.serializers.article_serializer import (ArticleSerializer, ArticleCardSerializer,
+ DefaultArticleCardImageSerializer, ArticleListSerializer,
+ ArticleCardlistSerializer, ArticleStatusChangeSerializer)
+
+USER = get_user_model()
+
+
+class ArticleViewSet(GenericViewSet, mixins.CreateModelMixin, mixins.UpdateModelMixin,
+ mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin):
+ """
+ article api
+ """
+ serializer_class = ArticleSerializer
+ permission_classes = [IsAuthenticated, AdminPermission]
+ queryset = Article
+ filter_backends = (SearchFilter,)
+ search_fields = ['title']
+
+ def get_queryset(self):
+ article = self.queryset.objects.filter(is_deleted=False).prefetch_related(
+ 'article_cards', 'article_survey', 'article_survey__options'
+ ).order_by('-created_at')
+ queryset = self.filter_queryset(article)
+ return queryset
+
+ def create(self, request, *args, **kwargs):
+ """
+ article create api method
+ :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)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return custom_response(SUCCESS_CODE["3027"])
+
+ def update(self, request, *args, **kwargs):
+ """
+ article update api method
+ :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()
+ serializer = self.serializer_class(article, data=request.data)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return custom_response(SUCCESS_CODE["3028"])
+
+ def list(self, request, *args, **kwargs):
+ """
+ article list api method
+ :param request:
+ :return: list of article
+ """
+ queryset = self.get_queryset()
+ count = queryset.count()
+ 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, count=count)
+
+ def retrieve(self, request, *args, **kwargs):
+ """
+ article detail api method
+ :param request: article_id
+ :return: article detail data
+ """
+ queryset = self.get_object()
+ serializer = self.serializer_class(queryset, many=False)
+ return custom_response(None, data=serializer.data)
+
+ def destroy(self, request, *args, **kwargs):
+ """
+ article delete (soft delete) api method
+ :param request: article_id
+ :return: success message
+ """
+ article = self.get_object()
+ article.is_deleted = True
+ article.save()
+ if article:
+ return custom_response(SUCCESS_CODE["3029"])
+ return custom_error_response(ERROR_CODE["2041"], status.HTTP_400_BAD_REQUEST)
+
+ @action(methods=['patch'], url_name='status-change', url_path='status-change',
+ detail=True, serializer_class=ArticleStatusChangeSerializer)
+ def article_status_change(self, request, *args, **kwargs):
+ """
+ article un-publish or publish api method
+ :param request: article id and
+ {
+ "is_published": true/false
+ }
+ :return: success message
+ """
+ article = Article.objects.filter(id=kwargs['pk']).first()
+ serializer = self.serializer_class(article, data=request.data)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return custom_response(SUCCESS_CODE["3038"])
+
+ @action(methods=['get'], url_name='remove-card', url_path='remove-card',
+ detail=True)
+ def remove_article_card(self, request, *args, **kwargs):
+ """
+ article card remove (delete) api method
+ :param request:
+ :param args:
+ :param kwargs:
+ :return: success message
+ """
+ try:
+ ArticleCard.objects.filter(id=kwargs['pk']).first().delete()
+ return custom_response(SUCCESS_CODE["3030"])
+ except AttributeError:
+ return custom_error_response(ERROR_CODE["2042"], response_status=status.HTTP_400_BAD_REQUEST)
+
+ @action(methods=['get'], url_name='remove-survey', url_path='remove-survey',
+ detail=True)
+ def remove_article_survey(self, request, *args, **kwargs):
+ """
+ article survey remove (delete) api method
+ :param request:
+ :param args:
+ :param kwargs:
+ :return: success message
+ """
+ try:
+ ArticleSurvey.objects.filter(id=kwargs['pk']).first().delete()
+ return custom_response(SUCCESS_CODE["3031"])
+ except AttributeError:
+ return custom_error_response(ERROR_CODE["2043"], response_status=status.HTTP_400_BAD_REQUEST)
+
+ @action(methods=['post'], url_name='test-add-card', url_path='test-add-card',
+ detail=False, serializer_class=ArticleCardSerializer, permission_classes=[AllowAny])
+ def add_card(self, request):
+ """
+ :param request:
+ :return:
+ """
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return custom_response(SUCCESS_CODE["3000"])
+
+ @action(methods=['get'], url_name='test-list-card', url_path='test-list-card',
+ detail=False, serializer_class=ArticleCardSerializer, permission_classes=[AllowAny])
+ def list_card(self, request):
+ """
+ :param request:
+ :return:
+ """
+ queryset = ArticleCard.objects.all()
+ serializer = self.serializer_class(queryset, many=True)
+ return custom_response(None, serializer.data)
+
+
+class DefaultArticleCardImagesViewSet(GenericViewSet, mixins.CreateModelMixin, mixins.ListModelMixin):
+ """
+ api to upload and list default article card images
+ """
+ serializer_class = DefaultArticleCardImageSerializer
+ permission_classes = [IsAuthenticated, AdminPermission]
+ queryset = DefaultArticleCardImage.objects.all()
+
+ def create(self, request, *args, **kwargs):
+ """
+ api method to upload default article card images
+ :param request: {
+ "image_name": "string",
+ "image": "image_file"
+ }
+ :return: success message
+ """
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return custom_response(SUCCESS_CODE["3000"])
+
+ def list(self, request, *args, **kwargs):
+ """
+ api method to list default article card images
+ :param request:
+ :return: default article card images
+ """
+ queryset = self.get_queryset()
+ serializer = self.serializer_class(queryset, many=True)
+ return custom_response(None, data=serializer.data)
+
+
+class ArticleListViewSet(GenericViewSet, mixins.ListModelMixin):
+ """
+ article api
+ """
+ serializer_class = ArticleListSerializer
+ permission_classes = [IsAuthenticated,]
+ queryset = Article
+ http_method_names = ['get',]
+
+ def get_queryset(self):
+ queryset = self.queryset.objects.filter(is_deleted=False, is_published=True).order_by('-created_at')
+ return queryset
+
+ def list(self, request, *args, **kwargs):
+ """
+ article list api method
+ :param request:
+ :param args:
+ :param kwargs:
+ :return: list of article
+ """
+ queryset = self.get_queryset()
+ serializer = self.serializer_class(queryset, context={"user": request.user}, many=True)
+ return custom_response(None, data=serializer.data)
+
+class ArticleCardListViewSet(viewsets.ModelViewSet):
+ """Article card list
+ use below query param
+ article_id"""
+ serializer_class = ArticleCardlistSerializer
+ permission_classes = [IsAuthenticated]
+ http_method_names = ('get',)
+
+ def get_queryset(self):
+ """get queryset"""
+ return ArticleCard.objects.filter(article=self.request.GET.get('article_id'))
+ def list(self, request, *args, **kwargs):
+ """Article card list
+ use below query param
+ article_id"""
+
+ try:
+ queryset = self.get_queryset()
+ # article card list
+ serializer = ArticleCardlistSerializer(queryset, context={"user": self.request.user,
+ "card_count": queryset.count()}, many=True)
+ return custom_response(None, serializer.data, response_status=status.HTTP_200_OK)
+ except Exception as e:
+ return custom_error_response(str(e), response_status=status.HTTP_400_BAD_REQUEST)
diff --git a/web_admin/views/auth.py b/web_admin/views/auth.py
new file mode 100644
index 0000000..36f058b
--- /dev/null
+++ b/web_admin/views/auth.py
@@ -0,0 +1,66 @@
+"""
+web_admin auth views file
+"""
+# django imports
+from rest_framework.viewsets import GenericViewSet
+from rest_framework.decorators import action
+from rest_framework import status
+from django.contrib.auth import get_user_model
+
+# local imports
+from account.utils import custom_response, custom_error_response
+from base.messages import SUCCESS_CODE, ERROR_CODE
+from web_admin.serializers.auth_serializer import (AdminOTPSerializer, AdminVerifyOTPSerializer,
+ AdminCreatePasswordSerializer)
+
+USER = get_user_model()
+
+
+class ForgotAndResetPasswordViewSet(GenericViewSet):
+ """
+ to reset admin password
+ """
+ queryset = None
+
+ @action(methods=['post'], url_name='otp', url_path='otp',
+ detail=False, serializer_class=AdminOTPSerializer)
+ def admin_otp(self, request):
+ """
+ api method to send otp
+ :param request: {"email": "string"}
+ :return: success message
+ """
+ serializer = self.serializer_class(data=request.data)
+ if serializer.is_valid():
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3015'])
+ return custom_error_response(ERROR_CODE['2000'], status.HTTP_400_BAD_REQUEST)
+
+ @action(methods=['post'], url_name='verify-otp', url_path='verify-otp',
+ detail=False, serializer_class=AdminVerifyOTPSerializer)
+ 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)
+ if serializer.is_valid():
+ return custom_response(SUCCESS_CODE['3011'])
+ return custom_error_response(ERROR_CODE['2008'], status.HTTP_400_BAD_REQUEST)
+
+ @action(methods=['post'], url_name='create-password', url_path='create-password',
+ detail=False, serializer_class=AdminCreatePasswordSerializer)
+ 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)
+ if serializer.is_valid():
+ user = USER.objects.filter(email=serializer.validated_data.get('email')).first()
+ user.set_password(serializer.validated_data.get('new_password'))
+ user.save()
+ return custom_response(SUCCESS_CODE['3006'])
+ return custom_error_response(ERROR_CODE['2064'], status.HTTP_400_BAD_REQUEST)
diff --git a/web_admin/views/user_management.py b/web_admin/views/user_management.py
new file mode 100644
index 0000000..b0b611e
--- /dev/null
+++ b/web_admin/views/user_management.py
@@ -0,0 +1,142 @@
+"""
+web_admin user_management views file
+"""
+# django imports
+from rest_framework.viewsets import GenericViewSet, mixins
+from rest_framework.filters import OrderingFilter, SearchFilter
+from rest_framework import status
+from rest_framework.decorators import action
+from rest_framework.permissions import IsAuthenticated, AllowAny
+from django.contrib.auth import get_user_model
+from django.db.models import Q
+
+# local imports
+from account.utils import custom_response, custom_error_response
+from base.constants import USER_TYPE, GUARDIAN, JUNIOR
+from base.messages import SUCCESS_CODE, ERROR_CODE
+from base.tasks import send_email
+from guardian.models import Guardian
+from junior.models import Junior
+from web_admin.permission import AdminPermission
+from web_admin.serializers.user_management_serializer import (UserManagementListSerializer,
+ UserManagementDetailSerializer, GuardianSerializer,
+ JuniorSerializer)
+
+USER = get_user_model()
+
+
+class UserManagementViewSet(GenericViewSet, mixins.ListModelMixin,
+ mixins.RetrieveModelMixin, mixins.UpdateModelMixin):
+ """
+ api to manage (list, view, edit) user
+ """
+ serializer_class = UserManagementListSerializer
+ permission_classes = [IsAuthenticated, AdminPermission]
+ queryset = USER.objects.filter(
+ (Q(junior_profile__is_verified=True) | Q(guardian_profile__is_verified=True)),
+ is_superuser=False).prefetch_related('guardian_profile',
+ 'junior_profile'
+ ).exclude(junior_profile__isnull=True,
+ guardian_profile__isnull=True).order_by('-date_joined')
+ filter_backends = (SearchFilter,)
+ search_fields = ['first_name', 'last_name']
+ http_method_names = ['get', 'post', 'patch']
+
+ def get_queryset(self):
+ if self.request.query_params.get('user_type') == dict(USER_TYPE).get('2'):
+ queryset = self.queryset.filter(junior_profile__isnull=True)
+ elif self.request.query_params.get('user_type') == dict(USER_TYPE).get('1'):
+ queryset = self.queryset.filter(guardian_profile__isnull=True)
+ else:
+ queryset = self.queryset
+ queryset = self.filter_queryset(queryset)
+ return queryset
+
+ def list(self, request, *args, **kwargs):
+ """
+ api method to list all the user
+ :param request: user_type {'guardian' for Guardian list, 'junior' for Junior list}
+ :return:
+ """
+ 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, count=queryset.count())
+
+ def retrieve(self, request, *args, **kwargs):
+ """
+ to get details of single user
+ :param request: user_id along with
+ user_type {'guardian' for Guardian, 'junior' for Junior} mandatory
+ :return: user details
+ """
+ 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
+ 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):
+ """
+ api method to update user detail (email, phone number)
+ :param request: user_id along with
+ user_type {'guardian' for Guardian, 'junior' for Junior} mandatory
+ :return: success message
+ """
+ if self.request.query_params.get('user_type') not in [dict(USER_TYPE).get('1'), dict(USER_TYPE).get('2')]:
+ return custom_error_response(ERROR_CODE['2067'], status.HTTP_400_BAD_REQUEST)
+ if self.request.query_params.get('user_type') == dict(USER_TYPE).get('2'):
+ guardian = Guardian.objects.filter(user_id=kwargs['pk'], is_verified=True
+ ).select_related('user').first()
+ serializer = GuardianSerializer(guardian,
+ request.data, context={'user_id': kwargs['pk']})
+
+ elif self.request.query_params.get('user_type') == dict(USER_TYPE).get('1'):
+ 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)
+ serializer.save()
+ return custom_response(SUCCESS_CODE['3037'])
+
+ @action(methods=['get'], url_name='change-status', url_path='change-status', detail=True)
+ def change_status(self, request, *args, **kwargs):
+ """
+ api to change user status (mark as active or inactive)
+ :param request: user_id along with
+ user_type {'guardian' for Guardian, 'junior' for Junior} mandatory
+ :return: success message
+ """
+ user_type = self.request.query_params.get('user_type')
+ if user_type not in [GUARDIAN, JUNIOR]:
+ return custom_error_response(ERROR_CODE['2067'], status.HTTP_400_BAD_REQUEST)
+
+ if user_type == GUARDIAN:
+ obj = Guardian.objects.filter(user_id=kwargs['pk'], is_verified=True).select_related('user').first()
+ elif user_type == JUNIOR:
+ obj = Junior.objects.filter(auth_id=kwargs['pk'], is_verified=True).select_related('auth').first()
+
+ if not obj:
+ return custom_error_response(ERROR_CODE['2004'], status.HTTP_400_BAD_REQUEST)
+
+ if obj.is_active:
+ deactivate_email_template = 'user_deactivate.email'
+ obj.is_active = False
+ send_email([obj.user.email if user_type == GUARDIAN else obj.auth.email],
+ deactivate_email_template)
+ else:
+ activate_email_template = 'user_activate.email'
+ obj.is_active = True
+ send_email([obj.user.email if user_type == GUARDIAN else obj.auth.email],
+ activate_email_template)
+ obj.save()
+ return custom_response(SUCCESS_CODE['3038'])
diff --git a/zod-bank-fcm-credentials.json b/zod-bank-fcm-credentials.json
new file mode 100644
index 0000000..75b13ab
--- /dev/null
+++ b/zod-bank-fcm-credentials.json
@@ -0,0 +1,13 @@
+{
+ "type": "service_account",
+ "project_id": "zod-bank-ebb2a",
+ "private_key_id": "f1115f1b1fece77ba7a119b70e2502ce0777a7d4",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCvLWEobyVN7D4+\nSZ4NwcugwuVk9MV7CjhQGB8lqzf/1Z0plBytjpjM75+orFk5n2tnOgTxsWCqR1R7\nLry4x2QH3HgJebd/TZTIyfepcAeuzUVhq9prgWVRsvxjpihMPZufA/Q1GEX5TBwX\nEasBW91Qwas2NBhUrzotnUBxOshVB4zCo3Ls9dbAN9O2O6paUMvcofSsRZ9XkB6P\nFFKy6nbQ3Bo+Lw3ntUfG1JQgkkxto2Vudiiq6J2dE6Eih2acEhEezQoJVpkMK+si\nlGp88T3j8nTx3o6ol99ke+3ZejPVE5sUbuhokSV/tS1Goy3whP+ys9lQtIyt3/mJ\nlmkoB9ShAgMBAAECggEAAk3H0GFF08C3EBWyDqH5dbLXbtH92e/UeNAMNcV4mGbY\n5GKGFywmEVmg2N22EPqa/s+Y6QxxrD9u9G1+EhbnIIx7olOBkScJ9C0c9CWztCon\ntPaofd1E1cI7I7UfVTg2ZLAHrBN4H70eadYc4va8bBtHj0EHYONz7S8sEBQ1Qna2\nIQuCEWY6MzhwCNEFIJd8ikd0GnkAJCuInK3F+2c37kugdgjRKxkTIfWmhNIfyWn3\ngoui5wltuuDKETVj9VUMyN6P7kffIJzS4mMRm/9F92scpPRbK+X1TrHpFsO826oP\nUuSBi5oOZYNyAvMBXGdnemoUNtAE+xg4kqwYJu5T0QKBgQDlbO08yH5MSLUxXZfL\nkCYg1mCUC4ySpL/qVFNuyvY+ex9gIg6gj4vEpK8s1lt0yvxrTng/MXxMMg7o9dO8\nmSlv/665twk/7NfkexJPWs1ph+54tud3Sa0TiVw6U5ObPMr1EYc7RnoELDR9Exc1\nUDJB+T3f3SGJicZn4pALyx132wKBgQDDd9lhqWLXJeG7JkInhLywuSVmnk077hY8\nxAfHqlO+/EEkM/YBtUvtC4KNiSkVftQwSM80NRiHlPvlWZ0G8l9RAfM/xxL/XUq6\nOu1fW1uJuukJgXFePV9SQLzfgd1fk1f/dKuiPSNhCzgTD7dFks0Ip6FD6/PCdN7x\neqRUqDccMwKBgQCTk1mW+7CiCTLkKjv2KScdgEhncnZd7bO1W8C/R7bVwgUQpVeb\nWDqjpvs3cDssCVYNAFDA9Wfq61hD6bzlV/AbpvARbfd5MzQ8OB4zBUmUVGfFJoIF\nbVLzeivlKNWNybETqs6+Bjt+a6Dnw1vuY0OwxE5Urb1g50rEkCvwKhsueQKBgQDB\nUt3rG46oX80cPkCbuUquNs/o6JRWu6m+u9s9/RYLBI6g8ctT8S2A6ytaNNgvbFsM\nzlYwunriTdW9Bp6p6jmfcyBUad4+NtTbz8BJ2Z91Xylwv1eS73xBa8nh/R0nlCEq\nhQfj1DgTmPcC0z5eT0z+TFzRQqK6JsEBcFzrZdvrxQKBgQDGEtWElG5XoDwnwO5e\nmfFLRKHfG+Xa6FChl2eeDIGOxv/Y6SMwT68t0gtnDFQGtcIMvC+kC/1Rv1v8yrOD\nCzEToh91Fw1c1eayNnsLq07qYnVWoMa7xFFSDhtBAnepqlU+81qaDvp+yILTIYSP\nwUuk3NpdJs2LkMetm5zJC9PJ2w==\n-----END PRIVATE KEY-----\n",
+ "client_email": "firebase-adminsdk-umcph@zod-bank-ebb2a.iam.gserviceaccount.com",
+ "client_id": "102629742363778142120",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-umcph%40zod-bank-ebb2a.iam.gserviceaccount.com",
+ "universe_domain": "googleapis.com"
+}
diff --git a/zod_bank/__init__.py b/zod_bank/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/zod_bank/asgi.py b/zod_bank/asgi.py
new file mode 100644
index 0000000..967882b
--- /dev/null
+++ b/zod_bank/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for ZOD_Bank project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'zod_bank.settings')
+
+application = get_asgi_application()
diff --git a/zod_bank/celery.py b/zod_bank/celery.py
new file mode 100644
index 0000000..039ea03
--- /dev/null
+++ b/zod_bank/celery.py
@@ -0,0 +1,50 @@
+"""
+Celery Basic configuration
+"""
+
+# python imports
+import os
+
+# third party imports
+from celery import Celery
+from dotenv import load_dotenv
+from celery.schedules import crontab
+
+# OR, the same with increased verbosity:
+load_dotenv(verbose=True)
+
+env_path = os.path.join(os.path.abspath(os.path.join('.env', os.pardir)), '.env')
+
+load_dotenv(dotenv_path=env_path)
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', os.getenv('SETTINGS'))
+
+# celery app
+app = Celery()
+
+app.config_from_object('django.conf:settings')
+
+# Load task modules from all registered Django apps.
+app.autodiscover_tasks()
+
+# scheduled task
+app.conf.beat_schedule = {
+ "expired_task": {
+ "task": "guardian.utils.update_expired_task_status",
+ "schedule": crontab(minute=0, hour=0),
+ },
+ 'notify_task_expiry': {
+ 'task': 'base.tasks.notify_task_expiry',
+ 'schedule': crontab(minute='0', hour='18'),
+ },
+ 'notify_top_junior': {
+ 'task': 'base.tasks.notify_top_junior',
+ 'schedule': crontab(minute='0', hour='*/2'),
+ },
+}
+
+
+@app.task(bind=True)
+def debug_task(self):
+ """ celery debug task """
+ print(f'Request: {self.request!r}')
diff --git a/zod_bank/settings.py b/zod_bank/settings.py
new file mode 100644
index 0000000..cde1918
--- /dev/null
+++ b/zod_bank/settings.py
@@ -0,0 +1,328 @@
+"""
+Django settings for ZOD_Bank project.
+
+Generated by 'django-admin startproject' using Django 3.0.14.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.0/ref/settings/
+"""
+# Django Import
+import os
+from dotenv import load_dotenv
+from datetime import timedelta
+from firebase_admin import initialize_app
+
+load_dotenv()
+# OR, the same with increased verbosity:
+load_dotenv(verbose=True)
+# env path
+env_path = os.path.join(os.path.abspath(os.path.join('.env', os.pardir)), '.env')
+load_dotenv(dotenv_path=env_path)
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+# OR, the same with increased verbosity:
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.getenv('SECRET_KEY')
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = os.getenv('DEBUG')
+ENV = os.getenv('ENV')
+
+# cors allow setting
+CORS_ORIGIN_ALLOW_ALL = False
+
+# Allow specific origins
+if ENV in ['dev', 'qa', 'stage']:
+ CORS_ALLOWED_ORIGINS = [
+ # backend base url
+ "https://dev-api.zodqaapp.com",
+ "https://qa-api.zodqaapp.com",
+ "https://stage-api.zodqaapp.com",
+
+ # frontend url
+ "http://localhost:3000",
+ "https://zod-dev.zodqaapp.com",
+ "https://zod-qa.zodqaapp.com",
+ "https://zod-stage.zodqaapp.com",
+ # Add more trusted origins as needed
+ ]
+if ENV == "prod":
+ CORS_ALLOWED_ORIGINS = []
+
+# allow all host
+ALLOWED_HOSTS = ['*']
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ # Add your installed Django apps here
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ # Add Django rest framework apps here
+ 'django_extensions',
+ 'storages',
+ 'drf_yasg',
+ 'corsheaders',
+ 'django.contrib.postgres',
+ 'rest_framework',
+ 'fcm_django',
+ 'django_celery_beat',
+ # Add your custom apps here.
+ 'django_ses',
+ 'account',
+ 'junior',
+ 'guardian',
+ 'notifications',
+ 'web_admin',
+ # 'social_django'
+]
+# define middle ware here
+MIDDLEWARE = [
+ # Add your middleware classes here.
+ 'corsheaders.middleware.CorsMiddleware',
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'account.custom_middleware.CustomMiddleware'
+]
+
+# define root
+ROOT_URLCONF = 'zod_bank.urls'
+
+# define templates
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+# define wsgi
+WSGI_APPLICATION = 'zod_bank.wsgi.application'
+# define rest frame work
+REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
+ # 'rest_framework.authentication.SessionAuthentication',
+ 'rest_framework.authentication.BasicAuthentication',
+ 'rest_framework_simplejwt.authentication.JWTAuthentication',],
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+ 'PAGE_SIZE': 10,
+}
+# define jwt token
+SIMPLE_JWT = {
+ 'ACCESS_TOKEN_LIFETIME': timedelta(hours=2, minutes=59, seconds=59, microseconds=999999),
+ 'REFRESH_TOKEN_LIFETIME': timedelta(hours=71, minutes=59, seconds=59, microseconds=999999),
+
+}
+# 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'),
+ 'USER':os.getenv('DB_USERNAME'),
+ 'PASSWORD':os.getenv('DB_PASSWORD'),
+ 'HOST':os.getenv('DB_HOST'),
+ 'PORT':os.getenv('DB_PORT'),
+ }
+}
+# define swagger setting
+SWAGGER_SETTINGS = {
+ "exclude_namespaces": [],
+ "api_version": '0.1',
+ "api_path": "",
+ "enabled_methods": [
+ 'get',
+ 'post',
+ 'put',
+ 'patch',
+ 'delete'
+ ],
+ "api_key": '',
+ "is_authenticated": True,
+ "is_superuser": False,
+
+ 'SECURITY_DEFINITIONS': {
+ "api_key": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header",
+ },
+ },
+}
+# Password validation
+# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
+
+# password validation
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+# database query logs settings
+# Allows us to check db hits
+# useful to optimize db query and hit
+LOGGING = {
+ "version": 1,
+ "filters": {
+ "require_debug_true": {
+ "()": "django.utils.log.RequireDebugTrue"
+ }
+ },
+ "handlers": {
+ "console": {
+ "level": "DEBUG",
+ "filters": [
+ "require_debug_true"
+ ],
+ "class": "logging.StreamHandler"
+ }
+ },
+ # database logger
+ "loggers": {
+ "django.db.backends": {
+ "level": "DEBUG",
+ "handlers": [
+ "console"
+ ]
+ }
+ }
+}
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.0/topics/i18n/
+
+# language code
+LANGUAGE_CODE = 'en-us'
+# time zone
+TIME_ZONE = 'UTC'
+# define I18N
+USE_I18N = True
+# define L10N
+USE_L10N = True
+# define TZ
+USE_TZ = True
+# cors header settings
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
+
+# cors allow method
+CORS_ALLOW_METHODS = (
+ 'DELETE',
+ 'GET',
+ 'OPTIONS',
+ 'PATCH',
+ 'POST',
+ 'PUT',
+)
+# cors allow header
+CORS_ALLOW_HEADERS = (
+ 'accept',
+ 'accept-encoding',
+ 'authorization',
+ 'content-type',
+ 'dnt',
+ 'origin',
+ 'account-agent',
+ 'x-csrftoken',
+ 'x-requested-with',
+)
+
+# CORS header settings
+CORS_EXPOSE_HEADERS = (
+ 'Access-Control-Allow-Origin: *',
+)
+
+# Firebase settings
+FIREBASE_APP = initialize_app()
+
+# fcm django settings
+FCM_DJANGO_SETTINGS = {
+ "APP_VERBOSE_NAME": "ZOD_Bank",
+ "ONE_DEVICE_PER_USER": False,
+ "DELETE_INACTIVE_DEVICES": True,
+ "UPDATE_ON_DUPLICATE_REG_ID": True,
+}
+
+"""Static files (CSS, JavaScript, Images)
+https://docs.djangoproject.com/en/3.0/howto/static-files/"""
+
+# google client id
+GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')
+# google client secret key
+GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET')
+
+# CELERY SETUP
+CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL')
+CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND')
+CELERY_ACCEPT_CONTENT = ['application/json']
+CELERY_TASK_SERIALIZER = 'json'
+CELERY_RESULT_SERIALIZER = 'json'
+
+# email settings
+EMAIL_BACKEND = os.getenv("MAIL_BACKEND")
+EMAIL_HOST = os.getenv("MAIL_HOST")
+EMAIL_PORT = os.getenv("MAIL_PORT")
+EMAIL_USE_TLS = os.getenv("MAIL_USE_TLS")
+EMAIL_HOST_USER = os.getenv("MAIL_HOST_USER")
+EMAIL_HOST_PASSWORD = os.getenv("MAIL_HOST_PASSWORD")
+EMAIL_FROM_ADDRESS = os.getenv("MAIL_FROM_ADDRESS")
+DEFAULT_ADDRESS = os.getenv("DEFAULT_ADDRESS")
+
+
+# ali baba cloud settings
+
+ALIYUN_OSS_ACCESS_KEY_ID = os.getenv('ALIYUN_OSS_ACCESS_KEY_ID')
+ALIYUN_OSS_ACCESS_KEY_SECRET = os.getenv('ALIYUN_OSS_ACCESS_KEY_SECRET')
+ALIYUN_OSS_BUCKET_NAME = os.getenv('ALIYUN_OSS_BUCKET_NAME')
+ALIYUN_OSS_ENDPOINT = os.getenv('ALIYUN_OSS_ENDPOINT')
+ALIYUN_OSS_REGION = os.getenv('ALIYUN_OSS_REGION')
+
+
+# define static url
+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')
diff --git a/zod_bank/urls.py b/zod_bank/urls.py
new file mode 100644
index 0000000..ba78b8f
--- /dev/null
+++ b/zod_bank/urls.py
@@ -0,0 +1,37 @@
+"""ZOD_Bank URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.0/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+# third-party import
+from django.conf.urls.static import static
+from django.contrib import admin
+from django.urls import path, include
+from drf_yasg import openapi
+from drf_yasg.views import get_schema_view
+from django.urls import path
+from django.conf import settings
+
+schema_view = get_schema_view(openapi.Info(title="Zod Bank API", default_version='v1'), public=True, )
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+ path('', include(('account.urls', 'account'), namespace='account')),
+ path('', include('guardian.urls')),
+ path('', include(('junior.urls', 'junior'), namespace='junior')),
+ path('', include(('notifications.urls', 'notifications'), namespace='notifications')),
+ path('', include(('web_admin.urls', 'web_admin'), namespace='web_admin')),
+]
+
+if settings.DEBUG:
+ urlpatterns += [(path('apidoc/', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'))]
diff --git a/zod_bank/wsgi.py b/zod_bank/wsgi.py
new file mode 100644
index 0000000..e6b5c4f
--- /dev/null
+++ b/zod_bank/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for ZOD_Bank project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'zod_bank.settings')
+
+application = get_wsgi_application()