django makemigrations override to create migration files with custom names - python

I have a python2.7 django project (I know, I am in 20th century!) that has some models in it. I need to override makemigrations so that the migration filenames are of the form 0001.py, 0002.py and so on, and not like 0001_initial.py, 0002_model1.py and so on which is by default what happens.
I already looked at how to create custom manage.py commands and I am able to override makemigrations command. Presently my code for the custom command (python2.7) looks like this:
path/to/project/app/management/commands/makemigrations.py
from django.core.management.commands.makemigrations import Command as CoreMakeMigrationsCommand
class Command(CoreMakeMigrationsCommand):
def handle(self, *args, **options):
super(Command, self).handle(*args, **options)
It is doing nothing more than calling the original makemigrations at the moment. I need to be able to modify the behaviour of how autodetector.py(which is part of the makemigrations flow) decides the naming of the files. In this file, there's method suggest_name as shown below:
#classmethod
def suggest_name(cls, ops):
"""
Given a set of operations, suggest a name for the migration they might
represent. Names are not guaranteed to be unique, but put some effort
into the fallback name to avoid VCS conflicts if possible.
"""
if len(ops) == 1:
if isinstance(ops[0], operations.CreateModel):
return ops[0].name_lower
elif isinstance(ops[0], operations.DeleteModel):
return "delete_%s" % ops[0].name_lower
elif isinstance(ops[0], operations.AddField):
return "%s_%s" % (ops[0].model_name_lower, ops[0].name_lower)
elif isinstance(ops[0], operations.RemoveField):
return "remove_%s_%s" % (ops[0].model_name_lower, ops[0].name_lower)
elif ops:
if all(isinstance(o, operations.CreateModel) for o in ops):
return "_".join(sorted(o.name_lower for o in ops))
return "auto_%s" % get_migration_name_timestamp()
The above gets called from here, in the same file in another method arrange_for_graph:
for i, migration in enumerate(migrations):
if i == 0 and app_leaf:
migration.dependencies.append(app_leaf)
if i == 0 and not app_leaf:
new_name = "0001_%s" % migration_name if migration_name else "0001_initial"
else:
new_name = "%04i_%s" % (
next_number,
migration_name or self.suggest_name(migration.operations)[:100],
)
I am new to overriding core files, and cannot figure out how to override only this part from my original custom command file, so that my requirements are met?
Also, please advise on how that is going to affect the subsequent calls to makemigrations, since they will be dependent on the new set of migrations files (with modified names).
Thanks

Ok, here is how I did it.
Create a makemigrations.py file (as shown in django docs for overriding manage.py commands), inside that create a Command class inheriting "django.core.management.commands.makemigrations.Command" and override the method write_migration_files, as show below:
from django.core.management.commands.makemigrations import Command as CoreMakeMigrationsCommand
class Command(CoreMakeMigrationsCommand):
def write_migration_files(self, changes):
for item in changes.values():
print('Overriding default names of migrations supplied by django core')
item[0].name = item[0].name.split('_')[0]
super(Command, self).write_migration_files(changes)

Unless you have hundreds of migrations, I think the easiest would be the following:
Create migrations one by one as you need but with the names decided by Django.
Rename the migrations:
Rename the files: 0002_migration_name.py -> 0002.py, etc.
Rename the references in the files, for example, replacing:
dependencies = [
('appname', '0002_migration_name'),
]
by:
dependencies = [
('appname', '0002'),
]
Only apply any migration when it, and all the previous ones, have the name you want.

Related

remove orphaned file from a model

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

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

Detect whether code is being run in the context of migrate/makemigrations command

I have a model with dynamic choices, and I would like to return an empty choice list if I can guarantee that the code is being run in the event of a django-admin.py migrate / makemigrations command to prevent it either creating or warning about useless choice changes.
Code:
from artist.models import Performance
from location.models import Location
def lazy_discover_foreign_id_choices():
choices = []
performances = Performance.objects.all()
choices += {performance.id: str(performance) for performance in performances}.items()
locations = Location.objects.all()
choices += {location.id: str(location) for location in locations}.items()
return choices
lazy_discover_foreign_id_choices = lazy(lazy_discover_foreign_id_choices, list)
class DiscoverEntry(Model):
foreign_id = models.PositiveIntegerField('Foreign Reference', choices=lazy_discover_foreign_id_choices(), )
So I would think if I can detect the run context in lazy_discover_foreign_id_choices then I can choose to output an empty choice list. I was thinking about testing sys.argv and __main__.__name__ but I'm hoping there's possibly a more reliable way or an API?
Here is a fairly non hacky way to do this (since django already creates flags for us) :
import sys
def lazy_discover_foreign_id_choices():
if ('makemigrations' in sys.argv or 'migrate' in sys.argv):
return []
# Leave the rest as is.
This should work for all cases.
A solution I can think of would be to subclass the Django makemigrations command to set a flag before actually performing the actual operation.
Example:
Put that code in <someapp>/management/commands/makemigrations.py, it will override Django's default makemigrations command.
from django.core.management.commands import makemigrations
from django.db import migrations
class Command(makemigrations.Command):
def handle(self, *args, **kwargs):
# Set the flag.
migrations.MIGRATION_OPERATION_IN_PROGRESS = True
# Execute the normal behaviour.
super(Command, self).handle(*args, **kwargs)
Do the same for the migrate command.
And modify your dynamic choices function:
from django.db import migrations
def lazy_discover_foreign_id_choices():
if getattr(migrations, 'MIGRATION_OPERATION_IN_PROGRESS', False):
return []
# Leave the rest as is.
It is very hacky but fairly easy to setup.
Using the django.db.models.signals.pre_migrate should be enough to detect the migrate command. The drawback is that you cannot use it at configuration stage.

Add custom restriction of folders in Plone

Can I add a custom restriction for the folder contents in Plone 4.1. Eg. Restrict the folder to contain only files with extensions like *.doc, *.pdf
I am aware of the general restrictions like file/ folder/ page / image which is available in Plone
Not without additional development; you'd have to extend the File type with a validator to restrict the mime types allowed.
Without going into the full detail (try for yourself and ask more questions here on SO if you get stuck), here are the various moving parts I'd implement if I were faced with this problem:
Create a new IValidator class to check for allowed content types:
from zope.interface import implements
from Products.validation.interfaces.IValidator import IValidator
class LocalContentTypesValidator(object):
implements(IValidator)
def __init__(self, name, title='', description='')
self.name = name
self.title = title or name
self.description = description
def __call__(value, *args, **kwargs):
instance = kwargs.get('instance', None)
field = kwargs.get('field', None)
# Get your list of content types from the aq_parent of the instance
# Verify that the value (*usually* a python file object)
# I suspect you have to do some content type sniffing here
# Return True if the content type is allowed, or an error message
Register an instance of your validotor with the register:
from Products.validation.config import validation
validation.register(LocalContentTypesValidator('checkLocalContentTypes'))
Create a new subclass of the ATContentTypes ATFile class, with a copy of the baseclass schema, to add the validator to it's validation chain:
from Products.ATContentTypes.content.file import ATFile, ATFileSchema
Schema = ATFileSchema.schema.copy()
Schema['file'].validators = schema['file'].validators + (
'checkLocalContentTypes',)
class ContentTypeRestrictedFile(ATFile):
schema = Schema
# Everything else you need for a custom Archetype
or just alter the ATFile schema itself if you want this to apply to all File objects in your deployment:
from Products.ATContentTypes.content.file import ATFileSchema
ATFileSchema['file'].validators = ATFileSchema['file'].validators + (
'checkLocalContentTypes',)
Add a field to Folders or a custom sub-class to store a list of locally allowed content types. I'd probably use archetypes.schemaextender for this. There is plenty of documentation on this these days, WebLion has a nice tutorial for example.
You'd have to make a policy decision on how you let people restrict mime-types here of course; wildcarding, free-form text, a vocabulary, etc.

Categories