diff --git a/apps/emails/migrations/0002_alter_email_extras_emailqueue_email_queue.py b/apps/emails/migrations/0002_alter_email_extras_emailqueue_email_queue.py new file mode 100644 index 0000000..406e20d --- /dev/null +++ b/apps/emails/migrations/0002_alter_email_extras_emailqueue_email_queue.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.6 on 2025-09-06 13:25 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emails', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='email', + name='extras', + field=models.JSONField(blank=True, default=dict, null=True), + ), + migrations.CreateModel( + name='EmailQueue', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('last_sent', models.DateTimeField(auto_now_add=True, verbose_name='last sent')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + ), + migrations.AddField( + model_name='email', + name='queue', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='emails.emailqueue'), + ), + ] diff --git a/apps/emails/models.py b/apps/emails/models.py index 1507985..0ab5dd2 100644 --- a/apps/emails/models.py +++ b/apps/emails/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import F from oauth2_provider.settings import oauth2_settings from apps.users.models import User @@ -6,6 +7,18 @@ from utils.models import BaseModel from django.utils.translation import gettext_lazy as _ +class EmailQueueQuerySet(models.query.QuerySet): + pass + + +class EmailQueue(models.Model): + title = models.CharField(_('title'), max_length=255) + user = models.ForeignKey(User, verbose_name=_('user'), on_delete=models.CASCADE) + last_sent = models.DateTimeField(_('last sent'), auto_now_add=True) + + objects = EmailQueueQuerySet.as_manager() + + class EmailQuerySet(models.QuerySet): pass @@ -27,6 +40,8 @@ class Email(BaseModel): priority = models.IntegerField(default=5) extras = models.JSONField(default=dict, null=True, blank=True) stats = models.IntegerField(default=StateChoices.INIT, choices=StateChoices.choices) + queue = models.ForeignKey(EmailQueue, on_delete=models.PROTECT, related_name='+', null=True, blank=True) + # bulk = models.ForeignKey("BulkEmail", null=True, blank=True, on_delete=models.PROTECT) diff --git a/apps/emails/serializers.py b/apps/emails/serializers.py index e4d58a3..922c66b 100644 --- a/apps/emails/serializers.py +++ b/apps/emails/serializers.py @@ -7,6 +7,7 @@ class EmailSerializer(serializers.ModelSerializer): class Meta: model = Email fields = ( + "queue", "title", "message", "user", diff --git a/apps/emails/tasks.py b/apps/emails/tasks.py index 8ba11b5..a6f2faf 100644 --- a/apps/emails/tasks.py +++ b/apps/emails/tasks.py @@ -1,6 +1,11 @@ +from django.db.models import F +from django.utils import timezone +import logging +from apps.emails.models import Email, EmailQueue from main import celery_app from utils.clients import gotify +logger = logging.getLogger(__name__) @celery_app.task def send_email(email_uuid): @@ -10,3 +15,32 @@ def send_email(email_uuid): email.state = email.StateChoices.DONE email.save() + + +@celery_app.task +def send_email_for_queued_events(): + logger.info('Sending email for queued events') + for email_queue in EmailQueue.objects.all(): + qs = Email.objects.filter(queue=email_queue, stats=Email.StateChoices.INIT, created_at_gte=F('queue__last_sent')) + if email_queue.last_sent <= timezone.now() - timezone.timedelta(hours=1) and qs.exists(): + from django.core.mail import send_mail + from django.conf import settings + send_mail( + "you have some queued task(s)", + f"you have {qs.count()} queued task(s)", + settings.EMAIL_HOST_USER, + ["xdshia49@gmail.com"], + fail_silently=False, + ) + email_queue.last_sent = timezone.now() + email_queue.save() + + +celery_app.conf.beat_schedule = { + 'print-every-minute': { + 'task': 'apps.emails.tasks.send_email_for_queued_events', + 'schedule': 10.0, # هر 60 ثانیه + # یا می‌توانید از crontab استفاده کنید: + # 'schedule': crontab(minute='*/1'), + }, +} diff --git a/main/settings.py b/main/settings.py index 0ed3d61..2552600 100644 --- a/main/settings.py +++ b/main/settings.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ 'crispy_bootstrap5', 'django_filters', 'jalali_date', + "django_celery_beat", # 'django_minio_backend.apps.DjangoMinioBackendConfig', # local apps diff --git a/requirements.in b/requirements.in index f45ad5d..75355a9 100644 --- a/requirements.in +++ b/requirements.in @@ -18,4 +18,6 @@ celery httpx django-redis django_minio_backend -xlrd \ No newline at end of file +xlrd +django-celery-beat +# aiosmtpd #?? \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 68399e0..9519d14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,32 +1,37 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile # +aiosmtpd==1.4.6 + # via -r requirements.in amqp==5.3.1 # via kombu -anyio==4.9.0 +anyio==4.10.0 # via httpx argon2-cffi==25.1.0 # via minio -argon2-cffi-bindings==21.2.0 +argon2-cffi-bindings==25.1.0 # via argon2-cffi -asgiref==3.8.1 +asgiref==3.9.1 # via # django # django-cors-headers -async-timeout==5.0.1 - # via redis +atpublic==6.0.1 + # via aiosmtpd attrs==25.3.0 # via + # aiosmtpd # jsonschema # referencing billiard==4.2.1 # via celery celery==5.5.3 - # via -r requirements.in -certifi==2025.6.15 + # via + # -r requirements.in + # django-celery-beat +certifi==2025.8.3 # via # httpcore # httpx @@ -36,7 +41,7 @@ cffi==1.17.1 # via # argon2-cffi-bindings # cryptography -charset-normalizer==3.4.2 +charset-normalizer==3.4.3 # via requests click==8.2.1 # via @@ -52,12 +57,15 @@ click-repl==0.3.0 # via celery crispy-bootstrap5==2025.6 # via -r requirements.in -cryptography==45.0.4 +cron-descriptor==2.0.6 + # via django-celery-beat +cryptography==45.0.7 # via jwcrypto -django==5.2.3 +django==5.2.6 # via # -r requirements.in # crispy-bootstrap5 + # django-celery-beat # django-cors-headers # django-crispy-forms # django-filter @@ -65,8 +73,11 @@ django==5.2.3 # django-minio-backend # django-oauth-toolkit # django-redis + # django-timezone-field # djangorestframework # drf-spectacular +django-celery-beat==2.8.1 + # via -r requirements.in django-cors-headers==4.7.0 # via -r requirements.in django-crispy-forms==2.4 @@ -83,15 +94,17 @@ django-oauth-toolkit==3.0.1 # via -r requirements.in django-redis==6.0.0 # via -r requirements.in -djangorestframework==3.16.0 +django-timezone-field==7.1 + # via django-celery-beat +djangorestframework==3.16.1 # via # -r requirements.in # drf-spectacular drf-spectacular==0.28.0 # via -r requirements.in -gevent==25.5.1 +gevent==25.8.2 # via -r requirements.in -greenlet==3.2.3 +greenlet==3.2.4 # via gevent gunicorn==23.0.0 # via -r requirements.in @@ -112,7 +125,7 @@ jalali-core==1.0.0 # via jdatetime jdatetime==5.2.0 # via django-jalali-date -jsonschema==4.24.0 +jsonschema==4.25.1 # via drf-spectacular jsonschema-specifications==2025.4.1 # via jsonschema @@ -120,7 +133,7 @@ jwcrypto==1.5.6 # via django-oauth-toolkit kombu==5.5.4 # via celery -minio==7.2.15 +minio==7.2.16 # via django-minio-backend oauthlib==3.3.1 # via django-oauth-toolkit @@ -128,9 +141,9 @@ packaging==25.0 # via # gunicorn # kombu -pillow==11.2.1 +pillow==11.3.0 # via -r requirements.in -prompt-toolkit==3.0.51 +prompt-toolkit==3.0.52 # via click-repl psycopg[binary,pool]==3.2.9 # via -r requirements.in @@ -142,13 +155,15 @@ pycparser==2.22 # via cffi pycryptodome==3.23.0 # via minio +python-crontab==3.3.0 + # via django-celery-beat python-dateutil==2.9.0.post0 # via celery python-decouple==3.8 # via -r requirements.in pyyaml==6.0.2 # via drf-spectacular -redis==6.2.0 +redis==6.4.0 # via # -r requirements.in # django-redis @@ -156,11 +171,11 @@ referencing==0.36.2 # via # jsonschema # jsonschema-specifications -requests==2.32.4 +requests==2.32.5 # via # -r requirements.in # django-oauth-toolkit -rpds-py==0.25.1 +rpds-py==0.27.1 # via # jsonschema # referencing @@ -170,16 +185,16 @@ sniffio==1.3.1 # via anyio sqlparse==0.5.3 # via django -typing-extensions==4.14.0 +typing-extensions==4.15.0 # via - # anyio + # cron-descriptor # jwcrypto # minio - # psycopg # psycopg-pool - # referencing tzdata==2025.2 - # via kombu + # via + # django-celery-beat + # kombu uritemplate==4.2.0 # via drf-spectacular urllib3==2.5.0 @@ -195,7 +210,7 @@ wcwidth==0.2.13 # via prompt-toolkit xlrd==2.0.2 # via -r requirements.in -zope-event==5.1 +zope-event==5.1.1 # via gevent zope-interface==7.2 # via gevent