I recently deployed a full stack project using Django, Gunicorn and Nginx. Briefly speaking, it is a website for a friend of mine, a space in which he can share and publish his recent activities (such as published albums, photoshoots and articles). I tried to automatize the information collections for populating a model instance by uploading a file (for a faster, cleaner and easier upload from the admin panel) using several Django signals.
My idea was: if I upload from the admin panel a perfectly formatted file (.docx), using signals and other functions I will be able to fill the information and that’s it. But, if I delete an instance (ie an Album or an Article), I’d like that this would have effects on files (I mean, delete files from the media folder on the server). Before posting my code, I’d like to tell that in developing this approach works perfectly and it’s the same for different models. Sadly, it does not in production. More precisely, the file upload works perfectly, the processing sometimes doesn’t (and I’m left with empty fields), the file deletion for some models works, for other does not at all.
settings.py
INSTALLED_APPS = [
'musicsite.apps.MusicsiteConfig',
...
]
apps.py
from django.apps import AppConfig
class MusicsiteConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'musicsite'
def ready(self):
from musicsite import signals
models.py
class Bio(models.Model):
class Meta:
verbose_name_plural = "Bio"
italian_bio = models.TextField(*nothing important*)
english_bio = models.TextField(*nothing important*)
last_updated = models.DateField(auto_now=True)
doc_file = models.FileField(upload_to="documents/", *nothing important*)
signals.py
@receiver(pre_save, sender=Bio)
def replace_file(sender, instance, **kwargs):
if not instance.pk:
return
try:
old_instance = Bio.objects.get(pk=instance.pk)
except Bio.DoesNotExist:
return
old_doc_name = os.path.basename(old_instance.doc_file.name) if (old_doc := old_instance.doc_file) else None
new_doc_name = os.path.basename(instance.doc_file.name) if (new_doc := instance.doc_file) else None
if old_doc_name and (not new_doc_name or old_doc_name != new_doc_name):
old_doc_path = Path(settings.MEDIA_ROOT) / old_doc.name
if old_doc_path.is_file():
safe_unlink(old_doc_path, retries=100)
@receiver(pre_delete, sender=Bio)
def delete_file(sender, instance, using, **kwargs):
if instance.doc_file:
doc_file_path = Path(settings.MEDIA_ROOT) / instance.doc_file.name
if os.path.isfile(doc_file_path):
safe_unlink(doc_file_path, retries=100)
@receiver(post_save, sender=Bio)
def process_biography_file(sender, instance, **kwargs):
if hasattr(instance, '_disable_signals') and instance._disable_signals:
return
temp_it, temp_en = "", ""
if instance.doc_file:
full_path = Path(settings.MEDIA_ROOT) / instance.doc_file.name
try:
temp_data = handle_file(full_path)
temp_it, temp_en = generate_biography(temp_data)
except Exception as e:
instance.delete()
raise ValidationError(f"Error! {e}")
with transaction.atomic():
if instance.italian_bio == "":
if temp_it != "":
instance.italian_bio = temp_it
else:
instance.delete()
raise ValidationError("The italian field should not be empty!")
if instance.english_bio == "":
if temp_it != "":
instance.english_bio = temp_en
else:
instance.delete()
raise ValidationError("The english field should not be empty!")
# PREVENTS INFINITE RECURSION
instance._disable_signals = True
instance.save(update_fields=['italian_bio', 'english_bio',])
instance._disable_signals = False
else:
if instance.italian_bio == "" or instance.english_bio == "":
instance.delete()
raise ValidationError("Fields cannot be empty! Upload a file or provide values for the admin fields!")