Welcome to the Consider This series, where we explore real-world business challenges and practical Django solutions. In this post, we tackle a common scenario: managing affiliates and automating wallet creation when their status changes to "approved."
Business Problem
Imagine you're building an investment platform where affiliates can join as agents or students. These affiliates need a dedicated wallet to track their earnings, but the wallet should only be created once their application is approved.
To solve this problem, we need an efficient, automated solution within Django project.
Assumptions
Before diving into the solution, here are some assumptions:
You can set up a Django project and apps.
You know how to define Django models and start the development server.
With these assumptions, let's jump straight into setting up the models for Affiliate and AffiliateWallet.
from django.db import models
from django.contrib.auth.models import AbstractUser
import uuid
def upload_to_unique(instance, filename):
# Generate a unique filename with a timestamp
base, ext = os.path.splitext(filename)
timestamp = timezone.now().strftime("%Y%m%d%H%M%S")
unique_id = uuid.uuid4().hex[:8] # Generate a short unique identifier
new_filename = f"{base}_{timestamp}_{unique_id}{ext}"
# Organize files into folders based on user ID
return f"affiliate_documents/{instance.user.id}/{new_filename}"
class User(AbstractUser):
email = models.EmailField(unique = True)
class Affiliate(models.Model):
STATUS_TYPE = ("pending", "approved", "rejected")
AFFILIATE_TYPE_CHOICES = ("agent", "student")
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="affiliate_user")
affiliate_type = models.CharField(max_length=20, choices=AFFILIATE_TYPE_CHOICES)
document = models.FileField(upload_to=upload_to_unique)
status = models.CharField(max_length=50, choices=STATUS_TYPE, default="pending")
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.email}"
class AffiliateWallet(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="affiliate_wallet")
credit = models.FloatField(default=0.0)
created = models.DateTimeField(default=timezone.now)
updated = models.DateTimeField(default=timezone.now)
def __str__(self):
return f"{self.user.email}"
The Affiliate model tracks each user's affiliate status, while the AffiliateWallet model manages their financial credits.
To ensure uploaded files are stored uniquely and organized by user, we define the upload_to_unique function.
How upload_to_unique works
Generating a Unique Filename:
The base name and extension are extracted from the uploaded file.
A timestamp and a unique identifier (UUID) are appended to ensure the filename is unique.
Organizing Files:
- Files are stored in a directory structure based on the user ID, making it easier to manage user-specific documents.
Why This Approach
Avoiding Filename Collisions: The unique identifier and timestamp ensure that files with the same name won't overwrite each other.
Improved Organization: Structuring file paths by user ID keeps the storage system tidy and easy to navigate.
Now, let’s go back to Affiliate problem, shall we?
Automating Wallet Creation
We need to capture changes in the affiliate's status and create a wallet only when the status changes to "approved." This is where Django signals come in handy.
What are Django Signals?
Django signals are events that are triggered when certain actions occur in your application. They enable different parts of your application to communicate and respond to these events, decoupling event emitters from event handlers. This makes your code cleaner and easier to maintain.
Django provides built-in signals such as pre_save, post_save, pre_delete, and post_delete, among others. You can also create custom signals by the way (not our focus today, maybe next time)
Using Django Signals to Handle Affiliate Status Change
# models.py
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.utils import timezone
@receiver(pre_save, sender=Affiliate)
def capture_affiliate_pre_save_state(sender, instance, **kwargs):
if instance.pk: # Ensure this is an update, not a new instance
# Fetch the current state from the database
current_instance = sender.objects.get(pk=instance.pk)
pre_save_state = {}
for field in instance._meta.fields:
pre_save_state[field.name] = getattr(current_instance, field.name)
# Attach the old state to the instance for comparison
instance._pre_save_state = pre_save_state
@receiver(post_save, sender=Affiliate)
def handle_affiliate_update(sender, instance, created, **kwargs):
if not created and hasattr(instance, '_pre_save_state'): # remember the _pre_save_state we attached above?
for field in instance._meta.fields:
old_value = instance._pre_save_state[field.name]
new_value = getattr(instance, field.name)
# we now will check if there is a value changed, the field affected is "status" and the new status value is "approved"
if old_value != new_value and field.name == "status" and new_value == "approved":
# Create affiliate wallet for the user
AffiliateWallet.objects.get_or_create(user=instance.user)
So what’s going on in the code above???
Pre-save Signal (capture_affiliate_pre_save_state)
This signal is triggered before an Affiliate instance is saved.
We check if the instance already exists (instance.pk) to ensure it’s an update, not a new instance.
The current state of the instance is fetched from the database and stored in a dictionary (pre_save_state). This is then attached to the instance for later comparison.
Post-save Signal (handle_affiliate_update)
This signal is triggered after an Affiliate instance is saved.
We check if the instance was not newly created and has the pre_save_state attached.
The code compares old and new values of all fields. If the status field has changed to "approved," it triggers the creation of an AffiliateWallet.
The get_or_create() method ensures that no duplicate wallet is created.
Why Use Signals?
Decoupling: Keeps the model logic clean by moving side effects to separate signal handlers.
Reusability: Signal handlers can be reused across different parts of the application.
Maintainability: Easier to manage and extend as the application grows.
Solution Take Away
Automation: Saves time by automatically creating wallets upon status approval.
Data Integrity: Prevents duplicate wallet creation using get_or_create().
Scalability: Cleanly separates business logic from view functions.
Unique File Saving Advantage: Ensures uploaded document files are uniquely named, preventing overwrites and maintaining a well-organized file system.
Conclusion
Automating processes like wallet creation enhances user experience and reduces administrative overhead. By leveraging Django signals, we build smarter, more scalable applications.
Stay tuned for the next post in the Consider This series, where we continue to solve real-world business challenges using Django!