Django Signals

Want to delete registered users who have not activated their accounts after a minute. Here is the code in my signals.py:

from django.dispatch import receiver
from django.utils import timezone
from datetime import timedelta
from django.core.mail import send_mail
from .models import CustomUser


@receiver(post_save, sender=CustomUser)
def schedule_deletion(sender, instance, created, **kwargs):    
    if created:
        print('We are going somewhere')
        instance.scheduled_deletion_time = timezone.now() + timezone.timedelta(minutes=1)
        print(instance.scheduled_deletion_time)
        print(timezone.now())
        instance.save(update_fields=['scheduled_deletion_time'])

@receiver(post_save, sender = CustomUser)  
def perform_scheduled_deletion(sender,instance,*args,**kwargs):
    if not instance.is_active and timezone.now() >= instance.scheduled_deletion_time:
        print(timezone.now())
        print(instance.is_active)
        instance.delete() ```

In my apps.py, I registered the signal like this:
``` from django.apps import AppConfig

class CoreConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'core'

    def ready(self):
        import core.signals ```

I imported the post_save from django.db.models.signals

I dont see any questions on your post.
Are you having a problem, or this is not running as you expect?

Signals arenā€™t what youā€™re thinking they are. Theyā€™re not a means for you to schedule events for later. Thatā€™s the purpose of modules like Celery Beats. Youā€™re not going to be able to do what you want to do here from within Django itself.

Side note: I deleted your duplicate post. Do not post the same question or issue multiple times. (See the forum FAQ.)

Not running as expected. Just got a reply informing me Iā€™d need Celery to actually delete the inactive user

Alright. Thank you very much for this

Iā€™m on a windows machine. I would like to know if windows supports Celery please.

@giftedmatrix, while Celery is technically the correct design choice here; it might be a little bit heavy for your requirements.
Maybe a better implementation would be to have the registration process still use a signal to remove all previous attempts older than some time_frame.

This would mean that the most recent registration attempt wonā€™t be removed until there is yet another attempt(and this one is older than the time_frame), but it also means you wonā€™t need the additional infrastructure and business logic to run Celery and will only ever have a few stale registration attempts on the DB.

To answer you original question, Celery will run anywhere your app will run. Here is a basic example of someone using it to help you wrap your head around the concept.

Unfortunately no.
This is from their installation page:

Celery is a project with minimal funding, so we donā€™t support Microsoft Windows. Please donā€™t open any issues related to that platform.

You can always use docker, or wsl in the other hand.

Thank you for this. Same thing with the django-crontab. Unless you dockerize your project and unfortunately Iā€™m not too familiar with the technology. Guess I have no choice but to dive into it.

It payoffs on the long journey.
Itā€™s always a good idea to take a look into some boilerplate projects.
Like cookiecutter-django that setups a lot for you. If you found thatā€™s has too much stuff, maybe look to some simple ones. I like some articles that breakdown the process so you understand it better.
These are some good articles that may help you on this journey:
Dockerizing Django and Celery;
Without celery: Dockerizing Django with Postgres, Gunicorn, and Nginx | TestDriven.io

Thank you for this. Would implement the solution you gave

But remember: Each decision you make its a tradeoff.
Sometimes itā€™s better to keep it simple until you need more complexity, control. @gcain answer is also really handy, and way easier to implement.

Opinion:
I donā€™t like signals, they are not always remembered when you check for logic. I prefer creating a function that does all the logic of saving, creating more stuff as needed, on a services.py file. This was taken from HackSoftware style guide, itā€™s really worth the read.

Alright. I really appreciate your feedback and time. Would look into the resources you have suggested. Thank you

1 Like

Actually, the situation is that they donā€™t support Windows, but it works mostly ok.

I dockerized my django project so I could use the django-crontab module. Iā€™m trying to delete users that are inactive after a minute from registration and also send emails to users reminding them to subscribe every month. Both functionalities are not working. I initiated the crontab using ā€˜ā€˜docker exec -ti django-cron python manage.py crontab addā€™ā€™. The ā€˜django-crontabā€™ is listed in my Installed apps in the settings.py file and also in the requirements.txt. Please Iā€™d really appreciate being shown what Iā€™m not getting right. These are the relevant files and settings:

SIGNALS.PY

from django.dispatch import receiver
from django.utils import timezone
from datetime import timedelta
from django.core.mail import send_mail
from .models import CustomUser


@receiver(post_save, sender=CustomUser)
def schedule_deletion(sender, instance, created, **kwargs):    
    if created and not instance.is_active:
        print('We are going somewhere')
        instance.scheduled_deletion_time = timezone.now() + timezone.timedelta(minutes=1)
        print(instance.scheduled_deletion_time)
        print(instance.is_active)
        CustomUser.objects.filter(email=instance.email).update(scheduled_deletion_time=instance.scheduled_deletion_time)

    if instance.is_active:
        instance.scheduled_deletion_time = None
        CustomUser.objects.filter(email=instance.email).update(scheduled_deletion_time=instance.scheduled_deletion_time)

    

@receiver(post_save, sender=CustomUser)
def send_subscription_reminder(sender, instance, created, **kwargs):
    if created:
        #Set the last reminder sent time to the current time
        instance.last_reminder_sent = timezone.now()
        instance.save(update_fields=['last_reminder_sent'])

    #Check if it's been 30 days since the last reminder was sent
    if instance.last_reminder_sent is None or (timezone.now() - instance.last_reminder_sent) >= timedelta(days=30):
        #Check if the user has not paid this month
        if not instance.paid_for_the_month:
            # Send a reminder email
            send_mail(
                'Subscription Reminder',
                'Hello {},\n\nThis is a reminder that your subscription is due. Please make your payment as soon as possible.\n\nThank you for using our service!'.format(instance.first_name),
                'techforjonah@gmail.com',
                [instance.email],
                fail_silently=False,
            )
            #Update the last reminder sent time
            instance.last_reminder_sent = timezone.now()
            instance.save()```


CRON.PY
from datetime import timedelta
from django.utils import timezone
from core.models import CustomUser
from core.signals import send_subscription_reminder


def send_sub_reminder():
    # Calculate the date 28 days ago from today
    twenty_eight_days_ago = timezone.now() - timedelta(days=28)

    # Get all users who haven't paid and haven't received a reminder in the last 28 days
    users_to_remind = CustomUser.objects.filter(has_paid_this_month=False, last_reminder_sent__lt=twenty_eight_days_ago)

    # Loop through the users and send them a reminder
    for user in users_to_remind:
        send_subscription_reminder(sender=CustomUser, instance=user)
    return None

def delete_inactive_user():
    users = CustomUser.objects.filter(is_active=False)
    for user in users:
        if timezone.now() >= user.scheduled_deletion_time:
            user.delete()
    return None


SETTINGS.PY
CRONJOBS = [
    ('0 0 1 * *', 'core.cron.send_sub_reminder'),
    ('* * * * *', 'core.cron.delete_inactive_user')
]


DOCKERFILE
FROM python:3.10.5-alpine

# install django-crontab
RUN apk add --update apk-cron && rm -rf /var/cache/apk/*
RUN alias py=python

#set your working directory
WORKDIR /usr/src/dormafrica

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# install psycopg2 dependencies
RUN apk update \
    && apk add postgresql-dev gcc python3-dev musl-dev

COPY ../dormafrica .
COPY ./requirements.txt .

RUN pip install -r requirements.txt

# django-crontab logfile
RUN mkdir /cron
RUN touch /cron/django_cron.log

EXPOSE 8000

CMD service cron start && python manage.py runserver 0.0.0.0:8000


DOCKER-COMPOSE.YML
version: "3.9"

services:
    webapp:
        build: ../dormafrica
        command: python manage.py runserver 0.0.0.0:8000
        container_name: django-cron
        volumes:
            - ../dormafrica:/usr/src/dormafrica
        env_file: ./.env
        ports:
            - 8000:8000
        depends_on:
            - db
    
    db:
        image: postgres:13.0-alpine
        volumes:
            - postgres_data:/var/lib/postgresql/data/
        environment:
            - POSTGRES_DB=****
            - POSTGRES_USER=****
            - POSTGRES_PASSWORD=****

volumes:
    postgres_data:

The django-crontab project is seriously out-of-date. I wouldnā€™t suggest using it without some careful investigation.

How critical is it that the deletion occurs roughly 60 seconds after creation? What if itā€™s 65 seconds? Is that ok? (In other words, what is the actual limit required here?)

I agree with one of the earlier suggestions - Iā€™d check to see if the activation occurs within 60 seconds. If it does, great. If it doesnā€™t, then delete the account. If they donā€™t even attempt to activate the account, then the account can wait until the scheduled task executes. I would only do a ā€œhard checkā€ to delete the accounts on a scheduled basis perhaps once every 5 minutes. Thereā€™s probably no real need to do it any more often than that.

(And I would absolutely remove all signal-based logic from this process. Itā€™s not helping you here.)