djangodjango-modelsdjango-signals

How do I prevent a user from creating multiple objects within 24 hours in Django


I'm trying to prevent users from creating more than one Mining object within a 24-hour period in my Django project. I chose to use Django signals, specifically the pre_save signal, to enforce this rule globally—regardless of whether the object is created through the admin panel, a view, or an API endpoint.

Here is my Mining model:

class Mining(models.Model):
    mining_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    amount_mine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES)

    duration_seconds = models.PositiveIntegerField(null=True, blank=True)
    verified = models.BooleanField(default=False)

    ip_address = models.GenericIPAddressField(null=True, blank=True)
    hardware_info = models.TextField(null=True, blank=True)

    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.user} mined {self.amount_mine} AFC on {self.created_at.strftime('%Y-%m-%d')}"

My Signals:

from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils import timezone
from datetime import timedelta
from .models import Mining

@receiver(pre_save, sender=Mining)
def prevent_multiple_mining_per_day(sender, instance, **kwargs):
    if not instance.pk:
        last_24_hours = timezone.now() - timedelta(hours=24)
        has_recent = Mining.objects.filter(
            user=instance.user,
            created_at__gte=last_24_hours
        ).exists()
        if has_recent:
            raise ValueError("User has already mined in the last 24 hours.")

I chose signals because I want this restriction to apply no matter where the object is created from—views, admin panel, or REST API, to ensure consistency across the whole app.

Even with the signal in place, I’m still able to create multiple Mining objects within 24 hours from the Django admin panel. The signal doesn't seem to prevent it as expected—no error is raised, and the objects are saved.


Solution

  • The reason it fails is that instance.pk will have a value for a field with a default value. An AutoField has no default value: it is set to None, and later the database assigns it a value. But the UUIDField thus does not do that: it generates a UUID eagerly.

    But probably, signals are not a good idea. An idea could be to just restrict people from creating two or more records for each day. This is a different restriction, since if the next day starts within a second, one can create two accounts within one second, and in another scenario, it can require waiting 48 hours. But the advantage is that it can be enforced by the database with:

    from django.db import models
    from django.db.models.functions import TruncDate
    
    
    class Mining(models.Model):
        # …
        class Meta:
            constraints = [
                models.UniqueConstraint(
                    TruncDate('created_at'), 'user_id', name='one_mining_per_day'
                )
            ]

    A UniqueConstraint [Django-doc] is normally enforced by the database, and therefore, unless the database check is disabled, it is probably not possible to insert two.

    You can use signals, but signals have a lot of caveats [django-antipatterns]. In that case, you can check if you are creating an object with:

    @receiver(pre_save, sender=Mining)
    def prevent_multiple_mining_per_day(sender, instance, **kwargs):
        if instance._state.adding:
            # …

    But this will not work for bulk creates.