remove orphaned file from a model - python

I have the following model:
class Class(models.Model):
title = models.CharField(max_length = 60)
video = models.FileField(
upload_to = class_files_custom_upload,
validators = [
FileExtensionValidator(['mp4', 'webm', 'mpg', 'mpeg', 'ogv']),
]
)
section = models.ForeignKey(Section, on_delete = models.CASCADE)
created = models.DateTimeField(auto_now_add = True)
class Meta:
verbose_name = 'clase'
verbose_name_plural = 'clases'
ordering = ['created']
def __str__(self):
return self.title
I create an instance of this model, but if I update the video field with another file of any instance, the previous saved file is orphaned and the file takes up space and I want to avoid it, deleting the file.
To do this I customize the file load, putting a callable in the upload_to:
def class_files_custom_upload(instance, filename):
try:
old_instance = Class.objects.get(id = instance.id)
old_instance.video.delete()
except Class.DoesNotExist:
pass
return os.path.join('courses/videos', generate_secure_filename(filename))
In this way I achieve my goal. But I have several models that save multimedia files, and for each one I have to customize the file load, practically doing a function almost equal to class_files_custom_upload, and the code repeats and this is not optimal at all.
I tried to create a reusable function that meets the goal of the class_files_custom_upload function, in various fields like ImageField and FileField, but I can't do it since the function receives only 2 parameters, instance and filename, which is too little data to achieve it.
The only way I managed to create that "function" that meets the goal and is reusable, was to create a validator:
def delete_orphaned_media_file(value):
old_instance = value.instance.__class__.objects.get(pk = value.instance.pk)
media_file_field = getattr(old_instance, value.field.name)
if not media_file_field.name == value.name: media_file_field.delete()
And it works, but after all it is a "validator", a "validator" is supposed to validate a field, not "that". My question is, is it good practice to do this?
Is there a better alternative to my solution? but that this alternative meets the objective of being reusable.
Any suggestion helps my learning, thanks.

One of the problem is that, two or more FileFields can refer to the same file. In the database a FileField stores the location of the file, so two or more columns can have the same file, therefore, just removing the old one is not (completely) safe.
You can for example make use of django-unused-media. You install this with:
$ pip install django-unused-media
Next you add this to the installed apps:
# settings.py
INSTALLED_APPS = [
# …,
'django_unused_media',
# …
]
Next you can run:
python3 manage.py cleanup_unused_media
this will look for files that are no longer referenced, and clean these up interactively.
You can also make a scheduled task (for example with cron), that runs with the --no-input flag:
python3 manage.py cleanup_unused_media --no-input

Related

How to attach a python file to each row(i.e. each data entry) in database formed using Django?

Ive used default django superuser to create an admin and created a DB using it. But now in that i want to add a python script to each data entry i.e. row. How do i do this???
Nothing special. Used basic Django.
There doesn't seem to be anything particularly complex here. You're just asking if you can use a field on the model to choose something; of course you can.
For instance:
# actions.py
def thing1(obj):
# do something with obj
def thing2(obj):
# do something else with obj
# models.py
from . import actions
ACTIONS = {
"do_thing_1": actions.thing1,
"do_thing_2": actions.thing2,
}
ACTION_CHOICES = [(action, action) for action in ACTIONS]
class MyModel(models.Model):
action = models.CharField(max_length=20, choices=ACTION_CHOICES)
def do_action(self):
return ACTIONS[self.action](self)

Django Custom Migration Not Executing

So I added a new "status" field to a django database table. This field needed a default value, so I defaulted it to "New", but I then added a custom migration file that calls the save() method on all of the objects in that table, as I have the save() overridden to check a different table and pull the correct status from that. However, after running this migration, all of the statuses are still set to "New", so it looks like the save isn't getting executed. I tested this by manually calling the save on all the objects after running the migration, and the statuses are updated as expected.
Here's the table model in models.py:
class SOS(models.Model):
number = models.CharField(max_length=20, unique=True)
...
# the default="New" portion is missing here because I have a migration to remove it after the custom migration (shown below) that saves the models
status = models.CharField(max_length=20)
def save(self, *args, **kwargs):
self.status = self.history_set.get(version=self.latest_version).status if self.history_set.count() != 0 else "New"
super(SOS, self).save(*args, **kwargs)
And here is the migration:
# Generated by Django 2.0.5 on 2018-05-23 13:50
from django.db import migrations, models
def set_status(apps, schema_editor):
SOS = apps.get_model('sos', 'SOS')
for sos in SOS.objects.all():
sos.save()
class Migration(migrations.Migration):
dependencies = [
('sos', '0033_auto_20180523_0950'),
]
operations = [
migrations.RunPython(set_status),
]
So it seems pretty clear to me that I'm doing something wrong with the migration, but I matched it exactly to what I see in the Django Documentation and I also compared it to this StackOverflow answer, and I can't see what I'm doing wrong. There are no errors when I run the migrations, but the custom one I wrote does run pretty much instanteously, which seems strange, as when I do the save manually, it takes about 5 seconds to save all 300+ entries.
Any suggestions?
P.S. Please let me know if there are any relevant details I neglected to include.
When you run migrations and get Model from apps you can not use custom managers or custom save or create or something like that. This model only have the fields and that's all. If you want to achieve what you want you should add your logic into you migrations like this:
# comment to be more than 6 chars...
def set_status(apps, schema_editor):
SOS = apps.get_model('sos', 'SOS')
for sos in SOS.objects.all():
if sos.history_set.exists():
sos.status = sos.history_set.get(version=sos.latest_version).status
else:
sos.status = "New"
sos.save()

Django FileField upload_to assistance

I'm new to python and trying to adapt to the OOP of Python. Could someone explain why the following is saving in a folder called 'None'? I want to upload an audio file in the admin page. This file gets stored in its own folder with the 'Vocab name'
class Vocab(models.Model):
module = models.ForeignKey(Modules, on_delete=models.CASCADE)
number = models.CharField(max_length = 250)
name = models.CharField(max_length = 250)
class VocabContent(models.Model):
vocab = models.ForeignKey(Vocab, on_delete=models.CASCADE)
audio = models.FileField(upload_to=vocab.name)
Running the following on shell.
>>> from module.models import Modules, Vocab, VocabContent
>>> vocab = VocabContent.objects.get(pk=1)
>>> vocab.vocab.name
'Numbers'
Numbers is the value i am looking for.
It's probably because the way you reference vocab.name is not defined when your model migration is run. I can't explain precisely why this happens but a solution would be to use a callable as your upload_to to evaluate it at runtime and get the value correctly, much like this other answer: Dynamic File Path in Django
So, for you, you could have something like:
import os
def get_upload_path(instance, filename):
return os.path.join("%s" % instance.vocab.name, filename)
class VocabContent(models.Model):
vocab = models.ForeignKey(Vocab, on_delete=models.CASCADE)
audio = models.FileField(upload_to=get_upload_path) # Important to NOT put the parenthesis after the function name
Which would result in a path that concatenates the vocab.name to your file name for every new file.

Prepopulate database with fixtures or with script?

I'm not an expert but I think is a good idea using a Class to define choices and to prepopulate the database with these choices. I think that make easier to change choices, etc
So in my models.py I have:
class City(models.Model):
name = models.CharField(max_length=32)
distance = models.SmallIntegerField(blank=True, null=True)
#etc
class OtherClass(models.Model):
name = models.CharField(max_length=32)
#etc
class UserProfile(models.Model):
name = models.CharField(max_length=32)
city = models.ForeignKey(City)
otherfield = models.ForeignKey(OtherClass)
#etc
UserProfile is what the users compile, City, OtherClass is where the programmer puts the options.
After the migration I have to create some City and OtherClass objects: they will be the options (and yes they have to be fixed).
I Just find out about the fixtures. Until now I was using a script:
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sitopossedimenti.settings')
import django
django.setup()
from core.models import *
def populate():
namecity1 = add_source('city1', None)
namecity2 = add_source('city2', None)
#etc
nameotherclass1 = add_otherclass('name1', #etc)
#etc some thousands more
def add_source(name, distance):
s = model.Source.objects.get_or_create(name=name, distance=distance)[0]
s.save()
return s
def add_otherclass:
#etc
if __name__ == '__main__':
print ("Starting myapp population script...")
populate()
For now the script works (about) and I'm afraid to change... but what do you think? Are the fixtures better? Why? There're differences?
As the saying goes, if it works don't fix it. Fixtures is the more usual method but no harm in using your own. If you were writing a new test case, you might want to use fixtures, but If I were you I would just let this be.
If you want a fully automated way of achieving the result, consider migration.RunPython. The linked document contains a full example which shows data being loaded. Obviously this will happen with ./manage.py migrate without the need of an additional step.
The advantage of using migrations.RunPython is that if you were to share your app with a colleague or install on a different server, the required data will automatically be loaded into the production server and the tests will also have full access to it in the test database.

Django models: Only permit one entry in a model?

I want to make some of my Django global settings configurable through the admin interface.
To that end, I've decided to set them as database fields, rather than in settings.py.
These are the settings I care about:
class ManagementEmail(models.Model):
librarian_email = models.EmailField()
intro_text = models.CharField(max_length=1000)
signoff_text = models.CharField(max_length=1000)
These are one-off global settings, so I only ever want there to be a single librarian_email, intro_text etc floating around the system.
Is there a way I can prevent admin users from adding new records here, without preventing them from editing the existing record?
I guess I can do this by writing a custom admin template for this model, but I'd like to know if there's a neater way to configure this.
Could I use something other than class, for example?
Thanks!
Please see this question on "keep[ing] settings in database", where the answer seems to be django-dbsettings
Update
Just thought of another option: you can create the following model:
from django.contrib.sites.models import Site
class ManagementEmail(models.Model):
site = models.OneToOneField(Site)
librarian_email = models.EmailField()
intro_text = models.CharField(max_length=1000)
signoff_text = models.CharField(max_length=1000)
Because of the OneToOneField field, you can only have one ManagementEmail record per site. Then, just make sure you're using sites and then you can pull the settings thusly:
from django.contrib.sites.models import Site
managementemail = Site.objects.get_current().managementemail
Note that what everyone else is telling you is true; if your goal is to store settings, adding them one by one as fields to a model is not the best implementation. Adding settings over time is going to be a headache: you have to add the field to your model, update the database structure, and modify the code that is calling that setting.
That's why I'd recommend using the django app I mentioned above, since it does exactly what you want -- provide for user-editable settings -- without making you do any extra, unnecessary work.
I think the easiest way you can do this is using has_add_permissions function of the ModelAdmin:
class ContactUsAdmin(admin.ModelAdmin):
form = ContactUsForm
def has_add_permission(self, request):
return False if self.model.objects.count() > 0 else super().has_add_permission(request)
You can set the above to be any number you like, see the django docs.
If you need more granularity than that, and make the class a singleton at the model level, see django-solo. There are many singleton implementations also that I came across.
For StackedInline, you can use max_num = 1.
Try django-constance.
Here are some useful links:
https://github.com/jezdez/django-constance
http://django-constance.readthedocs.org/en/latest/
I'd take a page out of wordpress and create a Model that support settings.
class Settings(models.Model):
option_name = models.CharField(max_length=1000)
option_value = models.CharField(max_length=25000)
option_meta = models.CharField(max_length=1000)
Then you can just pickle (serialize) objects into the fields and you'll be solid.
Build a little api, and you can be as crafty as wordpress and call. AdminOptions.get_option(opt_name)
Then you can just load the custom settings into the runtime, keeping the settings.py module separate, but equal. A good place to write this would be in an __init__.py file.
Just set up an GlobalSettings app or something with a Key and Value field.
You could easily prevent admin users from changing values by not giving them permission to edit the GlobalSettings app.
class GlobalSettingsManager(models.Manager):
def get_setting(self, key):
try:
setting = GlobalSettings.objects.get(key=key)
except:
raise MyExceptionOrWhatever
return setting
class GlobalSettings(models.Model):
key = models.CharField(unique=True, max_length=255)
value = models.CharField(max_length=255)
objects = GlobalSettingsManager()
>>> APP_SETTING = GlobalSettings.objects.get_setting('APP_SETTING')
There are apps for this but I prefer looking at them and writing my own.
You can prevent users from adding/deleting an object by overriding this method on your admin class:
ModelAdmin.has_add_permission(self, request)
ModelAdmin.has_delete_permission(self, request, obj=None)
Modification of #radtek answer to prevent deleting if only one entry is left
class SendgridEmailQuotaAdmin(admin.ModelAdmin):
list_display = ('quota','used')
def has_add_permission(self, request):
return False if self.model.objects.count() > 0 else True
def has_delete_permission(self, request, obj=None):
return False if self.model.objects.count() <= 1 else True
def get_actions(self, request):
actions = super(SendgridEmailQuotaAdmin, self).get_actions(request)
if(self.model.objects.count() <= 1):
del actions['delete_selected']
return actions
I had basically the same problem as the original poster describes, and it's easily fixed by overriding modelAdmin classes. Something similar to this in an admin.py file easily prevents adding a new object but allows editing the current one:
class TitleAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=TestModel.title):
return False
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=TestModel.title):
return True
This doesn't prevent a user from posting a form that edits data, but keeps things from happening in the Admin site. Depending on whether or not you feel it's necessary for your needs you can enable deletion and addition of a record with a minimum of coding.

Categories