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.)