From babd24833d82bf39790e31b86043c931ab9e4326 Mon Sep 17 00:00:00 2001 From: Bhavishya Chaturvedi Date: Tue, 14 Oct 2025 19:13:32 +0000 Subject: [PATCH 1/6] Mailer models --- .../mailer/models.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/employer_recommendation_system/mailer/models.py b/employer_recommendation_system/mailer/models.py index 71a83623..4e390f94 100644 --- a/employer_recommendation_system/mailer/models.py +++ b/employer_recommendation_system/mailer/models.py @@ -1,3 +1,43 @@ from django.db import models # Create your models here. + + +class EmailRecord(models.Model): + """ + Stores individual email addresses and their sending status. + Each row corresponds to one email address from the uploaded CSV file. + """ + email_address = models.EmailField(unique=True) + created_at = models.DateTimeField(auto_now_add=True) + status = models.CharField( + max_length=20, + choices=[ + ('pending'), + ('sent',), + ('failed',), + ], + default='pending' + ) + sent_at = models.DateTimeField(null=True, blank=True) + error_message = models.TextField(null=True, blank=True) + + def __str__(self): + return f"{self.email_address} - {self.status}" + + +class EmailContent(models.Model): + """ + Stores the common mail subject and body for a group of email addresses. + One EmailContent can be linked to multiple EmailRecords. + """ + subject = models.CharField(max_length=255) + mail_body = models.TextField() + email_records = models.ManyToManyField( + EmailRecord, + related_name='email_contents' + ) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"EmailContent (Subject: {self.subject})" From 73eff9381de534aaad61dbd96e32b83d179f3605 Mon Sep 17 00:00:00 2001 From: Bhavishya Chaturvedi Date: Tue, 14 Oct 2025 19:17:35 +0000 Subject: [PATCH 2/6] Mailer models --- employer_recommendation_system/mailer/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/employer_recommendation_system/mailer/models.py b/employer_recommendation_system/mailer/models.py index 4e390f94..306be42a 100644 --- a/employer_recommendation_system/mailer/models.py +++ b/employer_recommendation_system/mailer/models.py @@ -38,6 +38,11 @@ class EmailContent(models.Model): related_name='email_contents' ) created_at = models.DateTimeField(auto_now_add=True) + user_id = models.ForeignKey( + 'auth.User', + on_delete=models.CASCADE, + related_name='email_contents' + ) def __str__(self): return f"EmailContent (Subject: {self.subject})" From 730bee12d4502446c63821587b9ab1d532b7a3a8 Mon Sep 17 00:00:00 2001 From: Bhavishya Chaturvedi Date: Thu, 23 Oct 2025 06:47:40 +0000 Subject: [PATCH 3/6] Suggested changes implemented --- employer_recommendation_system/mailer/models.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/employer_recommendation_system/mailer/models.py b/employer_recommendation_system/mailer/models.py index 306be42a..63b92cb0 100644 --- a/employer_recommendation_system/mailer/models.py +++ b/employer_recommendation_system/mailer/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.contrib.auth.models import User # Create your models here. @@ -8,19 +9,22 @@ class EmailRecord(models.Model): Stores individual email addresses and their sending status. Each row corresponds to one email address from the uploaded CSV file. """ - email_address = models.EmailField(unique=True) + email_address = models.EmailField() created_at = models.DateTimeField(auto_now_add=True) status = models.CharField( max_length=20, choices=[ - ('pending'), - ('sent',), - ('failed',), + ('pending', 'Pending'), + ('sent', 'Sent'), + ('failed', 'Failed'), ], default='pending' ) sent_at = models.DateTimeField(null=True, blank=True) error_message = models.TextField(null=True, blank=True) + + class Meta: + indexes = [models.Index(fields=['email_address'])] def __str__(self): return f"{self.email_address} - {self.status}" @@ -38,8 +42,8 @@ class EmailContent(models.Model): related_name='email_contents' ) created_at = models.DateTimeField(auto_now_add=True) - user_id = models.ForeignKey( - 'auth.User', + user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name='email_contents' ) From d38d4ba759795cc270b6deaacdd2cf041cc52526 Mon Sep 17 00:00:00 2001 From: Bhavishya Chaturvedi Date: Thu, 23 Oct 2025 07:04:08 +0000 Subject: [PATCH 4/6] Suggested Changes implemented --- employer_recommendation_system/mailer/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/employer_recommendation_system/mailer/models.py b/employer_recommendation_system/mailer/models.py index 63b92cb0..8414cf3a 100644 --- a/employer_recommendation_system/mailer/models.py +++ b/employer_recommendation_system/mailer/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.contrib.auth.models import User +from django.contrib.auth.models import User # Create your models here. From 7d50bc86a0bbe2a437facf29f3eb6a5715130af2 Mon Sep 17 00:00:00 2001 From: Bhavishya Chaturvedi Date: Thu, 23 Oct 2025 07:08:24 +0000 Subject: [PATCH 5/6] Mailer Modules --- .../mailer/admin.py | 13 +++++++ .../mailer/tasks.py | 36 +++++++++++++++++++ .../mailer/utils.py | 16 +++++++++ .../mailer/views.py | 13 +++++++ 4 files changed, 78 insertions(+) create mode 100644 employer_recommendation_system/mailer/tasks.py create mode 100644 employer_recommendation_system/mailer/utils.py diff --git a/employer_recommendation_system/mailer/admin.py b/employer_recommendation_system/mailer/admin.py index 8c38f3f3..a9fe7914 100644 --- a/employer_recommendation_system/mailer/admin.py +++ b/employer_recommendation_system/mailer/admin.py @@ -1,3 +1,16 @@ from django.contrib import admin # Register your models here. +from .models import EmailRecord, EmailContent + +@admin.register(EmailRecord) +class EmailRecordAdmin(admin.ModelAdmin): + list_display = ('email_address', 'status', 'sent_at', 'error_message') + list_filter = ('status',) + search_fields = ('email_address',) + + +@admin.register(EmailContent) +class EmailContentAdmin(admin.ModelAdmin): + list_display = ('subject', 'created_at', 'user_id') + filter_horizontal = ('email_records',) diff --git a/employer_recommendation_system/mailer/tasks.py b/employer_recommendation_system/mailer/tasks.py new file mode 100644 index 00000000..d58fae47 --- /dev/null +++ b/employer_recommendation_system/mailer/tasks.py @@ -0,0 +1,36 @@ +from celery import shared_task +from django.utils import timezone +from django.core.mail import send_mail +from .models import EmailRecord, EmailContent + + +@shared_task(bind=True, max_retries=3, default_retry_delay=60) +def send_bulk_emails(self, email_content_id): + """ + Celery task to send bulk emails based on EmailContent and linked EmailRecords. + """ + try: + content = EmailContent.objects.get(pk=email_content_id) + records = content.email_records.all() + + for record in records: + try: + send_mail( + subject=content.subject, + message=content.mail_body, + from_email=None, + recipient_list=[record.email_address], + fail_silently=False, + ) + record.status = 'sent' + record.sent_at = timezone.now() + record.save() + except Exception as e: + record.status = 'failed' + record.error_message = str(e) + record.save() + + return f"Bulk email task completed for EmailContent ID {email_content_id}" + + except Exception as exc: + raise self.retry(exc=exc) diff --git a/employer_recommendation_system/mailer/utils.py b/employer_recommendation_system/mailer/utils.py new file mode 100644 index 00000000..8af95ab7 --- /dev/null +++ b/employer_recommendation_system/mailer/utils.py @@ -0,0 +1,16 @@ +import csv +from .models import EmailRecord + +def import_emails_from_csv(csv_file): + """ + Reads a CSV containing email addresses and returns a list of EmailRecord objects. + CSV must have a column named 'email'. + """ + records = [] + reader = csv.DictReader(csv_file.read().decode('utf-8').splitlines()) + for row in reader: + email = row.get('email') + if email: + record, _ = EmailRecord.objects.get_or_create(email_address=email) + records.append(record) + return records diff --git a/employer_recommendation_system/mailer/views.py b/employer_recommendation_system/mailer/views.py index 91ea44a2..6bd6d161 100644 --- a/employer_recommendation_system/mailer/views.py +++ b/employer_recommendation_system/mailer/views.py @@ -1,3 +1,16 @@ from django.shortcuts import render # Create your views here. +from django.shortcuts import redirect +from django.contrib import messages +from .models import EmailContent +from .tasks import send_bulk_emails + +def trigger_bulk_mail(request, content_id): + """ + Trigger Celery task to send emails asynchronously for a given EmailContent. + """ + content = EmailContent.objects.get(pk=content_id) + send_bulk_emails.delay(content.id) + messages.success(request, f"Email sending started for '{content.subject}'") + return redirect('admin:mailer_emailcontent_changelist') From 5e8c7ecce490850b55af7a5ce01722adabdc974f Mon Sep 17 00:00:00 2001 From: Bhavishya Chaturvedi Date: Thu, 23 Oct 2025 07:21:31 +0000 Subject: [PATCH 6/6] Settings.py & celery.py --- .../__init__.py | 3 +++ .../employer_recommendation_system/celery.py | 8 +++++++ .../settings.py | 23 ++++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 employer_recommendation_system/employer_recommendation_system/celery.py diff --git a/employer_recommendation_system/employer_recommendation_system/__init__.py b/employer_recommendation_system/employer_recommendation_system/__init__.py index e69de29b..9e0d95fd 100644 --- a/employer_recommendation_system/employer_recommendation_system/__init__.py +++ b/employer_recommendation_system/employer_recommendation_system/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ('celery_app',) \ No newline at end of file diff --git a/employer_recommendation_system/employer_recommendation_system/celery.py b/employer_recommendation_system/employer_recommendation_system/celery.py new file mode 100644 index 00000000..89406c5c --- /dev/null +++ b/employer_recommendation_system/employer_recommendation_system/celery.py @@ -0,0 +1,8 @@ +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'employer_recommendation_system.settings') + +app = Celery('employer_recommendation_system') +app.config_from_object('django.conf:settings', namespace='CELERY') +app.autodiscover_tasks() diff --git a/employer_recommendation_system/employer_recommendation_system/settings.py b/employer_recommendation_system/employer_recommendation_system/settings.py index 274d822c..88b02f84 100644 --- a/employer_recommendation_system/employer_recommendation_system/settings.py +++ b/employer_recommendation_system/employer_recommendation_system/settings.py @@ -261,4 +261,25 @@ # global settings/configuration for a Simple JWT SIMPLE_JWT = { "TOKEN_OBTAIN_SERIALIZER": "auth.serializers.CustomTokenObtainPairSerializer", -} \ No newline at end of file +} +# EMAIL_CONFIG is a dictionary in config.py +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = EMAIL_HOST +EMAIL_PORT = EMAIL_PORT +EMAIL_USE_TLS = EMAIL_USE_TLS +EMAIL_USE_SSL = EMAIL_USE_SSL +EMAIL_HOST_USER = EMAIL_HOST_USER +EMAIL_HOST_PASSWORD = EMAIL_HOST_PASSWORD + + +# ------------------------------------------------------- +# 14. CELERY + RABBITMQ SETTINGS (loaded from config.py) +# ------------------------------------------------------- +CELERY_BROKER_URL ='amqp://guest:guest@localhost:5672//' +#CELERY_RESULT_BACKEND = 'CELERY_RESULT_BACKEND', 'rpc://') +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' +CELERY_TIMEZONE = 'Asia/Kolkata' +CELERY_TASK_TRACK_STARTED = True # Tracks when a task is started +CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 min hard time limit