diff --git a/README.md b/README.md index f7a02f0..8d93a21 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A Django-based project management system that provides functionalities for proje - [Features App Wise](#features-app-wise) - [Management Commands](#management-commands) - [Django Admin Enhancements](#django-admin-enhancements) +- [Celery Tasks](#celery-tasks) +- [Unit Tests](#unit-test) - [How to Use](#how-to-use) - [Dependencies](#dependencies) - [Future Enhancements](#future-enhancements) @@ -37,6 +39,12 @@ muqadim_basic_user_app_django/ │ │ ├── projects_list.html │ │ ├── supervisor_login.html │ │ └── view_comments.html +│ ├── 📁 static/ +│ │ └── css/ +│ │ ├── create_project.css +│ │ ├── projects_list.css +│ │ ├── supervisor_login.css +│ │ └── view_comments.css │ ├── __init__.py │ ├── admin.py │ ├── apps.py @@ -53,6 +61,7 @@ muqadim_basic_user_app_django/ │ ├── settings.py │ ├── urls.py │ └── wsgi.py +│ ├── celery.py │ ├── 📁 user/ │ ├── 📁 management/ @@ -70,6 +79,13 @@ muqadim_basic_user_app_django/ │ │ ├── home.html │ │ ├── login.html │ │ └── signup.html +│ ├── 📁 static/ +│ │ └── css/ +│ │ ├── change_password.css +│ │ ├── edit_profile.css +│ │ ├── home.css +│ │ ├── login.css +│ │ └── signup.css │ ├── __init__.py │ ├── admin.py │ ├── admin_forms.py @@ -82,6 +98,7 @@ muqadim_basic_user_app_django/ │ ├── serializers.py │ ├── tests.py │ ├── urls.py +│ ├── tasks.py │ └── views.py │ ├── 📁 utils/ @@ -328,8 +345,62 @@ The app's URL blueprint provides paths for both conventional web views and API a Models like `CustomUser`, `DateTimeRecord`,`PROJECT`,`SUPERVISOR`,`COMMENTS` etc are registered. Customizations include field rearrangements, list displays, and filters. -## How to Use +## Celery-Tasks +1. Whenever a user signs up a welcome mail is sent to him ,MailTral service is being used for this purpose,you need to run redis server on one terminal and celery server in other as well for this purpose +#### For starting redis +```bash +redis server +``` + +### in other terminal +#### For starting the celery +```bash +celery -A UniManage worker --loglevel=info +``` +2. A reminder mail to get back to platform is sent to the user ,the period upon which this reminder mail is to be sent is decided in model reminder setting you can edit that value in the django admin,MailTral service is being used for this purpose,you need to run redis server ion one terminal and celery server in other and celery beat server on another terminal as well for this purpose +#### For starting redis +```bash +redis server +``` + +### in other terminal +#### For starting the celery +```bash +celery -A UniManage worker --loglevel=info +``` + +### in another terminal +#### For starting the celery beat +```bash +celery -A UniManage beat --loglevel=info +``` + +## Unit-Test +Unit Tests have been deployed for both models ,unit tests dont cover the whole project, total coverage of unit test for this whole project is 42% . +### You can run the unit tests through following commands +#### For User app +```bash +python3 manage.py test user +``` +#### For Project app +```bash +python3 manage.py test project +``` +#### For whole project +```bash +python3 manage.py test +``` +#### For Unit Test Coverage +```bash +coverage run manage.py test +``` +```bash +coverage report +``` + + +## How to Use 1. Set up a Django project. 2. Ensure Django's authentication system is set up. 3. Include the URL patterns in your project's configuration. @@ -393,10 +464,26 @@ python manage.py runserver Yet another Swagger generator. It's a great tool for creating API documentation with OpenAPI and Swagger. +- **redis** + + Redis is an open-source, in-memory data structure store, used as a database, cache, and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, and geospatial indexes with radius queries. + +- **celery** + + Celery is an asynchronous task queue/job queue based on distributed message passing. It is focused on real-time operation but supports scheduling as well. + +- **celery[beat]** + + Celery Beat is a scheduler that integrates with Celery. It allows you to run tasks at regular intervals, similar to cron jobs in Unix systems. + +- **django-countries** + + A Django application that provides a country field for models and forms. It allows you to easily add drop-downs in forms to select a country, and it includes a list of all countries with their names, ISO codes, and more. + To install all the dependencies, use the following pip command: ```bash -pip install django==4.2.3 djangorestframework django-rest-framework-simplejwt djoser drf-yasg +pip install django==4.2.3 djangorestframework django-rest-framework-simplejwt djoser drf-yasg redis celery celery[beat] django-countries ``` diff --git a/UniManage/__init__.py b/UniManage/__init__.py index e69de29..ac5e271 100644 --- a/UniManage/__init__.py +++ b/UniManage/__init__.py @@ -0,0 +1,5 @@ +# __init__.py + +from .celery import app as celery_app + +__all__ = ('celery_app',) diff --git a/UniManage/celery.py b/UniManage/celery.py new file mode 100644 index 0000000..1722e94 --- /dev/null +++ b/UniManage/celery.py @@ -0,0 +1,20 @@ +# celery.py + +import os +from celery import Celery +from django.conf import settings + + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'UniManage.settings') + +app = Celery('UniManage') + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) + +# celery.py (or wherever your Celery configurations are) diff --git a/UniManage/settings.py b/UniManage/settings.py index 8d4c80a..21eeef7 100644 --- a/UniManage/settings.py +++ b/UniManage/settings.py @@ -13,6 +13,7 @@ from pathlib import Path import datetime import os +from datetime import timedelta # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -39,6 +40,7 @@ 'rest_framework', 'djoser', 'drf_yasg', + 'model_utils', ] REST_FRAMEWORK = { @@ -132,7 +134,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'user.middleware.CustomAuthenticationMiddleware', - ] +] AUTH_USER_MODEL = 'user.CustomUser' @@ -144,3 +146,41 @@ os.path.join(BASE_DIR, "project/static"), os.path.join(BASE_DIR, "user/static") ] + +# settings.py + +# Celery configurations +CELERY_BROKER_URL = 'redis://localhost:6379/0' +CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' +CELERY_TIMEZONE = 'UTC' + +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + +# SMTP configuration +EMAIL_HOST = 'sandbox.smtp.mailtrap.io' +EMAIL_PORT = 587 # You can choose any of the provided ports, but 587 is commonly used for STARTTLS +EMAIL_USE_TLS = True +EMAIL_HOST_USER = '6bd0f1298b89cb' +EMAIL_HOST_PASSWORD = '75848b7312e651' + +CELERY_BEAT_SCHEDULE = { + 'check-login': { + 'task': 'user.tasks.check_last_login_and_send_email', + 'schedule': timedelta(minutes=1), # Run daily + }, +} + +CELERY_BEAT_MAX_LOOP_INTERVAL = 25 # Time in seconds. 600 seconds is 10 minutes. + +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://127.0.0.1:6379/1', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } +} diff --git a/db.sqlite3 b/db.sqlite3 index 8e627e4..a84e999 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/project/migrations/0001_initial.py b/project/migrations/0001_initial.py index 57feaf6..5ffe606 100644 --- a/project/migrations/0001_initial.py +++ b/project/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.3 on 2023-08-29 07:32 +# Generated by Django 4.2.3 on 2023-09-04 07:14 from django.db import migrations, models diff --git a/project/migrations/0002_initial.py b/project/migrations/0002_initial.py index 59a0d30..f92b316 100644 --- a/project/migrations/0002_initial.py +++ b/project/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.3 on 2023-08-29 07:32 +# Generated by Django 4.2.3 on 2023-09-04 07:14 from django.conf import settings from django.db import migrations, models @@ -10,8 +10,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('project', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('project', '0001_initial'), ] operations = [ diff --git a/project/tests.py b/project/tests.py index f625cca..2d146e5 100644 --- a/project/tests.py +++ b/project/tests.py @@ -19,7 +19,7 @@ def setUp(self): """Setup test data for Supervisor login tests.""" self.client = APIClient() self.user = CustomUser.objects.create_user(email='test@example.com', password='testpassword', username='test', - father_name='father', description='hell no') + father_name='father', description='hell no', country="Algeria") self.supervisor = Supervisor.objects.create(user=self.user, expertise="Testing") def test_supervisor_login(self): @@ -44,7 +44,8 @@ def setUp(self): """Setup test data for Comment view set tests.""" self.client = APIClient() self.user = CustomUser.objects.create_user(email='test3@example.com', password='testpassword', username='test3', - father_name='father3', description='description 3') + father_name='father3', description='description 3', + country="Algeria") self.supervisor = Supervisor.objects.create(user=self.user, expertise="QA") self.project = Project.objects.create(name="Test Project 2", description="A second test project", start_date="2023-01-02", end_date="2023-12-31", @@ -72,7 +73,7 @@ class ProjectSerializerTestCase(TestCase): def setUp(self): """Setup test data for Project serializer tests.""" self.user = CustomUser.objects.create(email="test@example.com", password="testpassword", username="testuser", - father_name="testfather", description="testdesc") + father_name="testfather", description="testdesc", country="Algeria") self.supervisor = Supervisor.objects.create(user=self.user) self.project_data = { 'name': 'Test Project', @@ -121,7 +122,7 @@ class CommentSerializerTestCase(TestCase): def setUp(self): """Setup test data for Comment serializer tests.""" self.user = CustomUser.objects.create(email="test2@example.com", password="testpassword2", username="testuser2", - father_name="testfather2", description="testdesc2") + father_name="testfather2", description="testdesc2", country="Algeria") self.supervisor = Supervisor.objects.create(user=self.user) self.project = Project.objects.create(name='Test Project', description='Test', start_date='2023-01-01', end_date='2023-12-31', supervisor=self.supervisor) @@ -159,7 +160,7 @@ def setUp(self): """Setup test data for Supervisor login serializer tests.""" self.user = CustomUser.objects.create_user(email="supervisor@example.com", password="supervisorpassword", username="supervisoruser", father_name="supervisorfather", - description="supervisordesc") + description="supervisordesc", country="Algeria") self.supervisor = Supervisor.objects.create(user=self.user) def test_valid_login(self): @@ -183,7 +184,7 @@ def test_invalid_login(self): def test_non_supervisor_login(self): user = CustomUser.objects.create_user(email="notasupervisor@example.com", password="testpassword3", username="nonsupervisoruser", father_name="nonsupervisorfather", - description="nonsupervisordesc") + description="nonsupervisordesc", country='Algeria') data = { 'email': 'notasupervisor@example.com', 'password': 'testpassword3' @@ -201,7 +202,7 @@ class ProjectsListViewTestCase(TestCase): def setUp(self): """Setup test data for ProjectsListView tests.""" self.user = CustomUser.objects.create_user(email='test@example.com', password='testpassword', username='test', - father_name='father', description='description') + father_name='father', description='description', country='Algeria') self.supervisor = Supervisor.objects.create(user=self.user, expertise="Testing") self.project1 = Project.objects.create(name="Test Project 1", supervisor=self.supervisor, start_date=date.today(), end_date=date.today() + timedelta(days=10)) @@ -229,7 +230,9 @@ def setUp(self): """Setup test data for ViewCommentsView tests.""" self.user = CustomUser.objects.create_user(email='commenter@example.com', password='testpassword', username='commenter', - father_name='father_commenter', description='description_commenter') + father_name='father_commenter', description='description_commenter', + country='Algeria' + ) self.supervisor = Supervisor.objects.create(user=self.user, expertise="QA") self.project = Project.objects.create(name="Test Project", supervisor=self.supervisor, start_date=date.today(), end_date=date.today() + timedelta(days=10)) diff --git a/user/admin.py b/user/admin.py index 1f10b02..e647592 100644 --- a/user/admin.py +++ b/user/admin.py @@ -1,8 +1,7 @@ from django.contrib.auth.admin import UserAdmin -from .models import CustomUser, DateTimeRecord from .admin_site import admin_site from django.contrib import admin -from .models import CustomUser, DateTimeRecord +from .models import CustomUser, DateTimeRecord, ReminderSetting def make_active(modeladmin, request, queryset): @@ -43,13 +42,13 @@ class CustomUserAdmin(UserAdmin): fieldsets = ( (None, {'fields': ( - 'username', 'email', 'description', 'father_name', 'password', 'software_engineering_experience', - 'last_profile_update', 'is_active', 'is_staff')}), + 'username', 'email', 'description', 'father_name', 'password', 'software_engineering_experience', + 'last_profile_update', 'is_active', 'is_staff')}), ) list_display = ( - 'email', 'description', 'username', 'father_name', 'first_name', 'last_name', 'software_engineering_experience', - 'last_profile_update') + 'email', 'description', 'username', 'father_name', 'first_name', 'last_name', 'software_engineering_experience', + 'last_profile_update') search_fields = ('email', 'first_name', 'last_name', 'father_name') list_filter = ('is_active', 'is_staff', 'software_engineering_experience') actions = [make_active, make_inactive] @@ -72,3 +71,24 @@ class DateTimeRecordAdmin(admin.ModelAdmin): admin_site.register(DateTimeRecord, DateTimeRecordAdmin) + + +class ReminderSettingAdmin(admin.ModelAdmin): + list_display = ['duration'] + + def has_add_permission(self, request): + # Disable addition if a ReminderSetting instance already exists + return not ReminderSetting.objects.exists() + + def has_delete_permission(self, request, obj=None): + # Disable deletion + return False + + # Hide the delete action + def get_actions(self, request): + actions = super().get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions + +admin_site.register(ReminderSetting, ReminderSettingAdmin) diff --git a/user/forms.py b/user/forms.py index 332187f..70e3813 100644 --- a/user/forms.py +++ b/user/forms.py @@ -2,7 +2,7 @@ from django.contrib.auth.forms import UserCreationForm from .models import CustomUser from utils.constants import USER_FORM_FIELDS -from utils.helpers import validate_password +from utils.helpers import validate_password, get_countries class CustomUserCreationForm(UserCreationForm): @@ -12,6 +12,12 @@ class CustomUserCreationForm(UserCreationForm): This form inherits from Django's built-in UserCreationForm and adds additional fields for the CustomUser. """ + country = forms.ChoiceField(choices=[]) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + countries = get_countries() + self.fields['country'].choices = [(country, country) for country in countries] class Meta: model = CustomUser diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py index 2e35ff4..042058e 100644 --- a/user/migrations/0001_initial.py +++ b/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.3 on 2023-08-29 07:32 +# Generated by Django 4.2.3 on 2023-09-04 07:14 import django.contrib.auth.models import django.contrib.auth.validators @@ -23,6 +23,17 @@ class Migration(migrations.Migration): ('converted_to_utc', models.BooleanField(default=False)), ], ), + migrations.CreateModel( + name='ReminderSetting', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('duration', models.PositiveIntegerField(default=30)), + ('duration_type', models.CharField(choices=[('seconds', 'Seconds'), ('minutes', 'Minutes'), ('hours', 'Hours'), ('days', 'Days')], default='minutes', max_length=7)), + ], + options={ + 'verbose_name_plural': 'Reminder Settings', + }, + ), migrations.CreateModel( name='CustomUser', fields=[ @@ -41,6 +52,7 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True, null=True)), ('software_engineering_experience', models.PositiveIntegerField(blank=True, null=True)), ('last_profile_update', models.DateTimeField(blank=True, null=True)), + ('country', models.CharField(max_length=100)), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], diff --git a/user/models.py b/user/models.py index 40adb02..cbca6d8 100644 --- a/user/models.py +++ b/user/models.py @@ -17,6 +17,7 @@ class CustomUser(AbstractUser): description = models.TextField(null=True, blank=True) software_engineering_experience = models.PositiveIntegerField(null=True, blank=True) last_profile_update = models.DateTimeField(null=True, blank=True) + country = models.CharField(max_length=100) REQUIRED_FIELDS = REQUIRED_USER_FIELDS @@ -46,3 +47,17 @@ class DateTimeRecord(models.Model): def __str__(self): """Return the datetime and the conversion status as the string representation of the DateTimeRecord.""" return f"{self.datetime} (converted to UTC: {self.converted_to_utc})" + + +class ReminderSetting(models.Model): + DURATION_CHOICES = ( + ('seconds', 'Seconds'), + ('minutes', 'Minutes'), + ('hours', 'Hours'), + ('days', 'Days'), + ) + duration = models.PositiveIntegerField(default=30) + duration_type = models.CharField(max_length=7, choices=DURATION_CHOICES, default='minutes') + + class Meta: + verbose_name_plural = "Reminder Settings" diff --git a/user/serializers.py b/user/serializers.py index 025d236..09f36c1 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -1,19 +1,27 @@ from rest_framework import serializers from .models import CustomUser -from utils.helpers import validate_password +from utils.helpers import validate_password, get_countries class CustomUserSerializer(serializers.ModelSerializer): class Meta: model = CustomUser - fields = ['id','username', 'email', 'father_name', 'description', 'software_engineering_experience', 'last_profile_update'] + fields = ['id', 'username', 'email', 'father_name', 'description', 'software_engineering_experience', + 'last_profile_update', 'country'] + class CustomUserRegistrationSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) + country = serializers.ChoiceField(choices=get_countries()) class Meta: model = CustomUser - fields = ['username','email', 'password', 'father_name', 'description', 'software_engineering_experience'] + fields = ['username', 'email', 'password', 'father_name', 'description', 'software_engineering_experience', + 'country'] + + def __init__(self, *args, **kwargs): + super(CustomUserRegistrationSerializer, self).__init__(*args, **kwargs) + self.fields['country'].choices = [(country, country) for country in get_countries()] def create(self, validated_data): user = CustomUser.objects.create_user( @@ -22,10 +30,12 @@ def create(self, validated_data): password=validated_data['password'], father_name=validated_data['father_name'], description=validated_data.get('description', None), - software_engineering_experience=validated_data.get('software_engineering_experience', None) + software_engineering_experience=validated_data.get('software_engineering_experience', None), + country=validated_data.get('country', None), ) return user + class ChangePasswordSerializer(serializers.Serializer): old_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True, validators=[validate_password]) diff --git a/user/tasks.py b/user/tasks.py new file mode 100644 index 0000000..7be90cd --- /dev/null +++ b/user/tasks.py @@ -0,0 +1,59 @@ +# tasks.py + +from celery import shared_task +from datetime import datetime, timedelta +from django.core.mail import send_mail +from django.conf import settings +from .models import CustomUser, ReminderSetting + + +@shared_task(bind=True) +def send_welcome_email(self, email, username): + try: + subject = "Welcome to Our Uni Management Site!" + message = f""" + +
+ +Hello {username},
+Thank you for registering with us. We're excited to have you on board!
+Best Regards,
+Your Team
+ + + """ + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email], fail_silently=False, html_message=message) + except Exception as e: + # Here you can log the error or even retry the task if necessary. + self.retry(countdown=60 * 5, exc=e, max_retries=3) + + +@shared_task(bind=True) +def check_last_login_and_send_email(self): + # Get the duration and duration_type from the ReminderSetting + try: + reminder_setting = ReminderSetting.objects.first() + duration = reminder_setting.duration + duration_type = reminder_setting.duration_type + except AttributeError: + # Default values in case the setting isn't found + duration = 1 + duration_type = 'minutes' # Setting default to days for this test + + if duration_type == 'seconds': + time_threshold = datetime.now() - timedelta(seconds=duration) + elif duration_type == 'hours': + time_threshold = datetime.now() - timedelta(hours=duration) + elif duration_type == 'days': + time_threshold = datetime.now() - timedelta(days=duration) + else: # default to minutes + time_threshold = datetime.now() - timedelta(minutes=duration) + + # Get all users who joined more than the time_threshold ago + users_to_notify = CustomUser.objects.filter(last_login__lt=time_threshold) + + for user in users_to_notify: + subject = "We appreciate you!" + message = f"Hello {user.username},\n\n We have noticed that its been a while since you last visited. We would love to see you back on our platform!" + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email]) diff --git a/user/tests.py b/user/tests.py index 70dc449..bdd083b 100644 --- a/user/tests.py +++ b/user/tests.py @@ -100,7 +100,8 @@ def setUp(self): 'email': 'test112234@example.com', 'father_name': 'testfather', 'description': 'test description', - 'software_engineering_experience': 2 + 'software_engineering_experience': 2, + 'country': 'Algeria' } self.user = CustomUser.objects.create(**self.user_attributes) self.serializer = CustomUserSerializer(instance=self.user) @@ -110,7 +111,7 @@ def test_successful_serialization(self): data = self.serializer.data self.assertEqual(set(data.keys()), {'id', 'username', 'email', 'father_name', 'description', 'software_engineering_experience', - 'last_profile_update'}) + 'last_profile_update', 'country'}) self.assertEqual(data['username'], self.user_attributes['username']) def test_successful_deserialization(self): @@ -136,7 +137,8 @@ def setUp(self): 'password': 'password123', 'father_name': 'testfather', 'description': 'test description', - 'software_engineering_experience': 2 + 'software_engineering_experience': 2, + 'country': 'Algeria' } def test_successful_serialization(self): @@ -197,7 +199,8 @@ def setUp(self): 'password': 'testpass123', 'email': 'testuser@example.com', 'father_name': 'testfather', - 'description': 'Test description' + 'description': 'Test description', + 'country': 'Algeria' } self.user = get_user_model().objects.create_user(**self.user_data) self.client.login(email=self.user_data['email'], password=self.user_data['password']) @@ -228,7 +231,8 @@ def test_edit_profile_post(self): 'username': 'updateduser', 'email': 'updateduser@example.com', 'father_name': 'updatedfather', - 'description': 'Updated description' + 'description': 'Updated description', + 'country': 'Updated Country' }) updated_user = get_user_model().objects.get(pk=self.user.pk) @@ -237,6 +241,7 @@ def test_edit_profile_post(self): self.assertEqual(updated_user.email, 'updateduser@example.com') self.assertEqual(updated_user.father_name, 'updatedfather') self.assertEqual(updated_user.description, 'Updated description') + self.assertEqual(updated_user.country, 'Updated Country') self.assertRedirects(response, reverse('home')) @@ -254,6 +259,7 @@ def setUp(self): self.father_name = "John Doe" self.description = "Test description" self.software_engineering_experience = 5 + self.country = "Algeria" self.user = CustomUser.objects.create_user( username=self.test_username, @@ -261,7 +267,8 @@ def setUp(self): password=self.test_password, father_name=self.father_name, description=self.description, - software_engineering_experience=self.software_engineering_experience + software_engineering_experience=self.software_engineering_experience, + country=self.country ) def test_signup_api(self): @@ -272,7 +279,8 @@ def test_signup_api(self): 'password': 'password123', 'father_name': 'John Doe', 'description': 'Just a test user.', - 'software_engineering_experience': 2 + 'software_engineering_experience': 2, + 'country': 'Algeria' } response = self.client.post(reverse('signup-api'), data) @@ -317,7 +325,8 @@ def setUp(self): 'password': 'password123', 'father_name': 'John Doe', 'description': 'Just a test user.', - 'software_engineering_experience': 2 + 'software_engineering_experience': 2, + 'country': 'Algeria' } self.user = CustomUser.objects.create_user(**self.user_data) diff --git a/user/urls.py b/user/urls.py index c20b2bd..1c74057 100644 --- a/user/urls.py +++ b/user/urls.py @@ -1,14 +1,16 @@ from django.urls import path -from .views import SignupView, LoginView, LogoutView, EditProfileView, ChangePasswordView, HomeView, ListUsersView, SignupAPIView, LoginAPIView,logout_api_view, EditProfileAPIView,ChangePasswordAPIView,ListUsersAPIView +from .views import SignupView, LoginView, LogoutView, EditProfileView, ChangePasswordView, HomeView, ListUsersView, \ + SignupAPIView, LoginAPIView, logout_api_view, EditProfileAPIView, ChangePasswordAPIView, ListUsersAPIView from .views import CustomTokenRefreshView + # URL patterns for the user management functionalities urlpatterns = [ - path('login/', LoginView.as_view(), name='login'), # URL pattern for user login - path('signup/', SignupView.as_view(), name='signup'), # URL pattern for user registration - path('logout/', LogoutView.as_view(), name='logout'), # URL pattern for user logout + path('login/', LoginView.as_view(), name='login'), # URL pattern for user login + path('signup/', SignupView.as_view(), name='signup'), # URL pattern for user registration + path('logout/', LogoutView.as_view(), name='logout'), # URL pattern for user logout path('edit_profile/', EditProfileView.as_view(), name='edit_profile'), # URL pattern for editing user profiles path('change_password/', ChangePasswordView.as_view(), name='change_password'), # URL for changing user passwords - path('home/', HomeView.as_view(), name='home'), # URL pattern for rendering the home page + path('home/', HomeView.as_view(), name='home'), # URL pattern for rendering the home page path('api/users/', ListUsersView.as_view(), name='list-users'), path('api/signup/', SignupAPIView.as_view(), name='signup-api'), diff --git a/user/views.py b/user/views.py index 6642533..a0eb5ce 100644 --- a/user/views.py +++ b/user/views.py @@ -15,8 +15,8 @@ from django.contrib.auth import authenticate, login, logout from .serializers import (CustomUserSerializer, CustomUserRegistrationSerializer, ChangePasswordSerializer) from rest_framework.permissions import AllowAny - - +from .tasks import send_welcome_email +from utils.helpers import get_countries from utils.constants import ( SIGNUP_TEMPLATE, LOGIN_TEMPLATE, @@ -38,12 +38,22 @@ class SignupView(View): @staticmethod def get(request): form = CustomUserCreationForm() + countries = get_countries() # Fetch countries from cache + form.fields['country'].choices = [(country, country) for country in countries] # Set the country choices return render(request, SIGNUP_TEMPLATE, {'form': form}) @staticmethod def post(request): form = CustomUserCreationForm(request.POST) - return validate_and_save_form(form, request, 'login', SIGNUP_TEMPLATE, VALIDATION_ERROR_MSG) + countries = get_countries() # Fetch countries from cache + form.fields['country'].choices = [(country, country) for country in countries] # Set the country choices + + response = validate_and_save_form(form, request, 'login', SIGNUP_TEMPLATE, VALIDATION_ERROR_MSG) + if form.is_valid(): + send_welcome_email.delay(email=form.cleaned_data.get('email'), + username=form.cleaned_data.get('username')) + + return response class LoginView(View): @@ -133,6 +143,8 @@ def post(self, request): serializer = CustomUserRegistrationSerializer(data=request.data) if serializer.is_valid(): serializer.save() + send_welcome_email.delay(email=serializer.validated_data.get('email'), + username=serializer.validated_data.get('username')) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -169,6 +181,7 @@ def post(self, request): return Response({"detail": "Invalid Credentials"}, status=status.HTTP_401_UNAUTHORIZED) + @api_view(['GET']) @permission_classes([IsAuthenticated]) def logout_api_view(request): diff --git a/utils/constants.py b/utils/constants.py index 144d030..c1f6479 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -1,4 +1,3 @@ - # Constants related to URLs/templates paths SIGNUP_TEMPLATE = 'user/signup.html' LOGIN_TEMPLATE = 'user/login.html' @@ -18,7 +17,7 @@ ] # Constants related to the CustomUser model -REQUIRED_USER_FIELDS = ['father_name', 'email', 'description'] +REQUIRED_USER_FIELDS = ['father_name', 'email', 'description', 'country'] DEFAULT_SOFTWARE_ENGINEERING_EXPERIENCE = 0 # Constants related to the CustomUser forms @@ -27,7 +26,9 @@ 'username', 'father_name', 'software_engineering_experience', - 'description' + 'description', + 'country' + ) PASSWORD_MIN_LENGTH = 6 PASSWORD_DIGIT_MESSAGE = 'Password must contain at least 1 number.' @@ -109,3 +110,5 @@ # Name for the token refresh endpoint TOKEN_REFRESH_NAME = 'token_refresh' + +COUNTRY_NAME_INDEX = 1; diff --git a/utils/helpers.py b/utils/helpers.py index 081fa0a..4a50643 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -1,7 +1,10 @@ from django.shortcuts import render, redirect from django.core.exceptions import ValidationError from django.utils import timezone -from utils.constants import PROTECTED_VIEW_NAMES, DEFAULT_SOFTWARE_ENGINEERING_EXPERIENCE, PASSWORD_MIN_LENGTH, PASSWORD_DIGIT_MESSAGE, PASSWORD_LENGTH_MESSAGE +from utils.constants import PROTECTED_VIEW_NAMES, DEFAULT_SOFTWARE_ENGINEERING_EXPERIENCE, PASSWORD_MIN_LENGTH, \ + PASSWORD_DIGIT_MESSAGE, PASSWORD_LENGTH_MESSAGE, COUNTRY_NAME_INDEX +from django.core.cache import cache +from django_countries import countries def render_with_error(request, template, form, error_msg): @@ -52,3 +55,14 @@ def validate_password(password): if not any(char.isdigit() for char in password): raise ValidationError(PASSWORD_DIGIT_MESSAGE) return password + + +def get_countries(): + countries_list = cache.get('countries_list') + + if not countries_list: + # This is just a sample list. Use a comprehensive one or fetch it from a source. + countries_list = [country[COUNTRY_NAME_INDEX] for country in countries] + cache.set('countries_list', countries_list, 86400) # Cache for 24 hours + + return countries_list