Say I have the following model:
class DenyList(models.Model):
vm_id = models.CharField(max_length=25)
I want to add choices to the vm_id field, however the choices are dynamic via API of an external virtual machine registry system.
so I have that done in the following way:
class MachineChoices(object):
def get_vms(self):
if 'makemigrations' in sys.argv or 'migrate' in sys.argv:
return []
# calls api, optionally cache, return the vms
def __iter__(self):
yield from self.get_vms()
class DenyList(models.Model):
vm_id = models.CharField(max_length=25, choices=MachineChoices())
The above works that:
When creating migrations it won't call the API to set all the options in the migration file
It is driven from the backend so it works on all model forms so as in django admin.
Django admin displays the label instead of the raw value in list display (requires caching implemented in MachineChoices for efficiency)
I don't feel this is elegant as it involves hackery to deceive django especially during migrations.
I am aware that an alternative is to use autocomplete libraries but I need to customize with django forms.
So are there any better ways to do this?
You can set choices to None (or empty list) in your AppConfig instead.
myapp/apps.py:
import sys
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'myapp'
def ready(self):
if 'makemigrations' in sys.argv or 'migrate' in sys.argv:
from . import models
models.Company.street_num.field.choices = None
If you don't want to configure each model field specifically:
if 'makemigrations' in sys.argv or 'migrate' in sys.argv:
for model in self.models.values():
for field in model._meta.fields:
field.choices = None
Remember to include your AppConfig in INSTALLED_APPS.
mysite/settings.py:
INSTALLED_APPS = [
# ...
'myapp.apps.MyAppConfig',
]
Related
I'm using IntegerChoices in my Django (3.2) model.
class AType(db.IntegerChoices):
UNKNOWN = 0, 'Unknown'
SOMETHING = 1, 'Something'
ANOTHER_THING = 2, 'Another thing'
A_THIRD_THING = 3, 'A third thing'
class MyObject(models.Model):
a_type = db.IntegerField(choices=AType.choices)
(I've changed the choices to be more generic.)
Every time I add a value to AType, it produces a DB migration, which I faithfully apply.
a_type is strictly behind the scenes. Users never see it, so it's only in the admin UI, but I don't need it to be editable. So forms aren't really used.
Is there any impact on the DB (e.g., constraints) of these migrations?
Is there any other utility to having an IntegerChoices field, given that it's not displayed to a (non-staff) user, and not in a form?
If there's no utility, I'm thinking of just changing MyObject.a_type to an IntegerField, and continuing to use AType everywhere, but not having all the migrations.
Is there any impact on the DB (e.g., constraints) of these migrations?
No impact to the schema. You can see this in python manage.py sqlmigrate myapp 000x_mymigration.
However, it still does CREATE TABLE, INSERT INTO ... SELECT (expensive), DROP TABLE, ALTER TABLE.
This is "by design" and "wontfix":
#22837 (Migrations detect unnessecary(?) changes) — Django
#30048 (Migrations created when verbose_name or help_text is changed) – Django
Is there any other utility to having an IntegerChoices field, given that it's not displayed to a (non-staff) user, and not in a form?
Yes, model validation.
Reference: https://docs.djangoproject.com/en/3.2/ref/models/fields/#choices
I'm thinking of just changing MyObject.a_type to an IntegerField, and continuing to use AType everywhere, but not having all the migrations.
You can ignore choices by patching MigrationAutodetector in makemigrations and migrate.
You can additionally ignore _verbose_name and help_text.
mysite/apps.py:
from django.apps import AppConfig
from django.core.management.commands import makemigrations, migrate
from django.db import models
from django.db.migrations import autodetector
class MigrationAutodetector(autodetector.MigrationAutodetector):
ignored_field_attribute_names = [
'choices',
# '_verbose_name',
# 'help_text',
]
def deep_deconstruct(self, obj):
if isinstance(obj, models.Field):
for attr_name in self.ignored_field_attribute_names:
setattr(obj, attr_name, None)
return super().deep_deconstruct(obj)
class MySiteAppConfig(AppConfig):
name = 'mysite'
def ready(self):
makemigrations.MigrationAutodetector = MigrationAutodetector
migrate.MigrationAutodetector = MigrationAutodetector
pass
mysite/settings.py:
INSTALLED_APPS = [
# ...
'mysite.apps.MySiteAppConfig',
]
I need to update an existing project to Django 1.5 to take advantage of its newly available custom user model. However, I'm having trouble migrating reusable apps that contain a model with a foreign key to a user. Currently, the foreign key points to auth.User but with a custom user model, it needs to point to myapp.CustomUser. Hence, some kind of migration is needed. I can't simply create a migration file for it because its a reusable app. It wouldn't be future proof because each time the app is updated, I would need to remember to create that migration again (there might even be migration conflicts) so it's not exactly a plausible solution.
Is there a solution to this problem other than to, maybe, fork each project, add a migration file, and then use that instead?
Some code:
models.py in reusable app
from django.conf import settings
from django.db import models
UserModel = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
class ModelA(models.Model):
user = models.ForeignKey(UserModel)
models.py in my project
from django.conf import settings
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
...
settings.py in my project
AUTH_USER_MODEL = 'myapp.CustomUser'
So if the reusable app has a migration that creates a foreign key to a user, the following can be done to support Django 1.5's custom user model.
try:
from django.contrib.auth import get_user_model
except ImportError: # django < 1.5
from django.contrib.auth.models import User
else:
User = get_user_model()
class Migration(SchemaMigration):
def forwards(self, orm):
db.create_table('reusableapp.modela', (
('user', self.gf('django...ForeignKey')(to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])
models = {
...
# this should replace "auth.user"
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
'Meta': {'object_name': User.__name__},
}
"reusableapp.modela": {
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)})
}
}
I'm not sure if this is the best solution but it's being used in apps such as django-reversion.
However, this solution still can pose a problem if you originally started with auth.User and then changed to myapp.customuser, simply because south is honors AUTH_USER_MODEL but the migration for the custom user model hasn't been created yet. This can occur during testing. Ticket #1179 of south addresses this issue (http://south.aeracode.org/ticket/1179).
I extended Django admin site for my app to allow non-staff/superusers access. This is working just fine.
I created a proxy model for an existing model and registered it to my admin site, however, it doesn't appear for non-staff users. From the documentation I read, my understanding is that proxy models get their own permissions. I checked and these don't appear in the list of available permissions.
Here's my code in case it helps:
Normal Model
class Engagement(models.Model):
eng_type = models.CharField(max_length=5)
environment = models.CharField(max_length=8)
is_scoped = models.BooleanField()
class Meta:
ordering = ['eng_type', 'environment']
app_label = 'myapp'
Proxy Model
class NewRequests(Engagement):
class Meta:
proxy = True
app_label = 'myapp'
verbose_name = 'New Request'
verbose_name_plural = 'New Requests'
Model Admin
class NewRequestsAdmin(ModelAdmin):
pass
def queryset(self, request):
return self.model.objects.filter(is_scoped=0)
Custom Admin Registration
myapps_admin_site.register(NewRequests, NewRequestsAdmin)
I've been managing my DB with South. According to this post, you have to tamper with it a bit by following the instructions it points users to. This was a failure. My DB doesn't have a whole lot of info in it, so I uncommented South and ran a regular syncdb to rule out South. Unfortunately, this is still not working and I'm at a loss. Any help is appreciated.
Edit
This was on Django 1.4
Turns out I didn't do anything wrong. I was looking for the permissions under
myapp | New Request | Can add new request
Permissions fall under the parent model.
myapp | engagement | Can add new request
This is fixed in Django 2.2, quoting release notes:
Permissions for proxy models are now created using the content type of the proxy model rather than the content type of the concrete model. A migration will update existing permissions when you run migrate.
and docs:
Proxy models work exactly the same way as concrete models. Permissions are created using the own content type of the proxy model. Proxy models don’t inherit the permissions of the concrete model they subclass.
There is a workaround, you can see it here: https://gist.github.com/magopian/7543724
It can vary based on your django version, but the priciple is the same.
Tested with Django 1.10.1
# -*- coding: utf-8 -*-
"""Add permissions for proxy model.
This is needed because of the bug https://code.djangoproject.com/ticket/11154
in Django (as of 1.6, it's not fixed).
When a permission is created for a proxy model, it actually creates if for it's
base model app_label (eg: for "article" instead of "about", for the About proxy
model).
What we need, however, is that the permission be created for the proxy model
itself, in order to have the proper entries displayed in the admin.
"""
from __future__ import unicode_literals, absolute_import, division
import sys
from django.contrib.auth.management import _get_all_permissions
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand
from django.apps import apps
from django.utils.encoding import smart_text
class Command(BaseCommand):
help = "Fix permissions for proxy models."
def handle(self, *args, **options):
for model in apps.get_models():
opts = model._meta
ctype, created = ContentType.objects.get_or_create(
app_label=opts.app_label,
model=opts.object_name.lower(),
defaults={'name': smart_text(opts.verbose_name_raw)})
for codename, name in _get_all_permissions(opts):
p, created = Permission.objects.get_or_create(
codename=codename,
content_type=ctype,
defaults={'name': name})
if created:
sys.stdout.write('Adding permission {}\n'.format(p))
How to use
create a directory /myproject/myapp/management/commands
create the file /myproject/myapp/management/__init__.py
create the file /myproject/myapp/management/commands/__init__.py
save the code above into /myproject/myapp/management/commands/fix_permissions.py
run /manage.py fix_permissions
This is a known bug in Django: https://code.djangoproject.com/ticket/11154 (check comments for some patches)
As of 2021 and Django 3+, the solution for missing permissions for proxy model is simple, just generate migrations with makemigrations:
app#e31a3ffef22c:~/app$ python manage.py makemigrations my_app
Migrations for 'main':
main/migrations/0193_myproxymodel.py
- Create proxy model MyProxyModel
I came here and wasn't really sure, what is the correct cause/solution to this problem.
For Django 1.11
This issue is related due to the wrong content_type_id in auth_permission table.
By default, it adds the content type of the base model instead of proxy model content type.
I realize this question was closed a while ago, but I'm sharing what worked for me in case it might help others.
It turns out that even though permissions for the proxy models I created were listed under the parent apps (as #chirinosky) has mentioned, and even though I granted my non-super user all permissions, it was still denied access to my proxy models through the admin.
What I had to do was workaround a known Django bug (https://code.djangoproject.com/ticket/11154) and connect to the post_syncdb signal to properly create permissions for the proxy models. The code below is modified from https://djangosnippets.org/snippets/2677/ per some of the comments on that thread.
I placed this in myapp/models.py that held my proxy models. Theoretically this can live in any of your INSTALLED_APPS after django.contrib.contenttypes because it needs to be loaded after the update_contenttypes handler is registered for the post_syncdb signal so we can disconnect it.
def create_proxy_permissions(app, created_models, verbosity, **kwargs):
"""
Creates permissions for proxy models which are not created automatically
by 'django.contrib.auth.management.create_permissions'.
See https://code.djangoproject.com/ticket/11154
Source: https://djangosnippets.org/snippets/2677/
Since we can't rely on 'get_for_model' we must fallback to
'get_by_natural_key'. However, this method doesn't automatically create
missing 'ContentType' so we must ensure all the models' 'ContentType's are
created before running this method. We do so by un-registering the
'update_contenttypes' 'post_syncdb' signal and calling it in here just
before doing everything.
"""
update_contenttypes(app, created_models, verbosity, **kwargs)
app_models = models.get_models(app)
# The permissions we're looking for as (content_type, (codename, name))
searched_perms = list()
# The codenames and ctypes that should exist.
ctypes = set()
for model in app_models:
opts = model._meta
if opts.proxy:
# Can't use 'get_for_model' here since it doesn't return
# the correct 'ContentType' for proxy models.
# See https://code.djangoproject.com/ticket/17648
app_label, model = opts.app_label, opts.object_name.lower()
ctype = ContentType.objects.get_by_natural_key(app_label, model)
ctypes.add(ctype)
for perm in _get_all_permissions(opts, ctype):
searched_perms.append((ctype, perm))
# Find all the Permissions that have a content_type for a model we're
# looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create.
all_perms = set(Permission.objects.filter(
content_type__in=ctypes,
).values_list(
"content_type", "codename"
))
objs = [
Permission(codename=codename, name=name, content_type=ctype)
for ctype, (codename, name) in searched_perms
if (ctype.pk, codename) not in all_perms
]
Permission.objects.bulk_create(objs)
if verbosity >= 2:
for obj in objs:
sys.stdout.write("Adding permission '%s'" % obj)
models.signals.post_syncdb.connect(create_proxy_permissions)
# See 'create_proxy_permissions' docstring to understand why we un-register
# this signal handler.
models.signals.post_syncdb.disconnect(update_contenttypes)
I am working through a Django tutorial at http://lightbird.net/dbe/todo_list.html . I completed Django's official tutorial. When I attempted to sync
from django.db import models
from django.contrib import admin
class Item(models.Model):
name = models.CharField(max_length=60)
created = models.DateTimeField(auto_now_add=True)
priority = models.IntegerField(default=0)
difficult = models.IntegerField(default=0)
class ItemAdmin(admin.ModelAdmin):
list_display = ["name", "priority", "difficult", "created", "done"]
search_fields = ["name"]
admin.site.register(Item, ItemAdmin)
The terminal came back with an error that said admin was not defined. What am I doing wrong? How do I define admin?
Update: I added the whole models file as it looks now
The Django documentation lists multiple steps that need to be done to activate the admin site:
Add 'django.contrib.admin' to your INSTALLED_APPS setting.
The admin has four dependencies - django.contrib.auth, django.contrib.contenttypes, django.contrib.messages and
django.contrib.sessions. If these applications are not in your
INSTALLED_APPS list, add them.
Add django.contrib.messages.context_processors.messages to TEMPLATE_CONTEXT_PROCESSORS and MessageMiddleware to
MIDDLEWARE_CLASSES. (These are both active by default, so you only
need to do this if you’ve manually tweaked the settings.)
Determine which of your application’s models should be editable in the admin interface.
For each of those models, optionally create a ModelAdmin class that encapsulates the customized admin functionality and options for
that particular model.
Instantiate an AdminSite and tell it about each of your models and ModelAdmin classes.
Hook the AdminSite instance into your URLconf. lists a couple of things that you must do the enable Django's admin site.
If you haven't done the first two items, syncdb might complain because it can't find the admin app itself.
In Django, you can specify relationships like:
author = ForeignKey('Person')
And then internally it has to convert the string "Person" into the model Person.
Where's the function that does this? I want to use it, but I can't find it.
As of Django 1.11 to 4.0 (at least), it's AppConfig.get_model(model_name, require_ready=True)
As of Django 1.9 the method is django.apps.AppConfig.get_model(model_name).
-- danihp
As of Django 1.7 the django.db.models.loading is deprecated (to be removed in 1.9) in favor of the the new application loading system.
-- Scott Woodall
Found it. It's defined here:
from django.db.models.loading import get_model
Defined as:
def get_model(self, app_label, model_name, seed_cache=True):
django.db.models.loading was deprecated in Django 1.7 (removed in 1.9) in favor of the the new application loading system.
Django 1.7 docs give us the following instead:
>>> from django.apps import apps
>>> User = apps.get_model(app_label='auth', model_name='User')
>>> print(User)
<class 'django.contrib.auth.models.User'>
just for anyone getting stuck (like I did):
from django.apps import apps
model = apps.get_model('app_name', 'model_name')
app_name should be listed using quotes, as should model_name (i.e. don't try to import it)
get_model accepts lower case or upper case 'model_name'
Most model "strings" appear as the form "appname.modelname" so you might want to use this variation on get_model
from django.db.models.loading import get_model
your_model = get_model ( *your_string.split('.',1) )
The part of the django code that usually turns such strings into a model is a little more complex This from django/db/models/fields/related.py:
try:
app_label, model_name = relation.split(".")
except ValueError:
# If we can't split, assume a model in current app
app_label = cls._meta.app_label
model_name = relation
except AttributeError:
# If it doesn't have a split it's actually a model class
app_label = relation._meta.app_label
model_name = relation._meta.object_name
# Try to look up the related model, and if it's already loaded resolve the
# string right away. If get_model returns None, it means that the related
# model isn't loaded yet, so we need to pend the relation until the class
# is prepared.
model = get_model(app_label, model_name,
seed_cache=False, only_installed=False)
To me, this appears to be an good case for splitting this out into a single function in the core code. However, if you know your strings are in "App.Model" format, the two liner above will work.
2020 solution:
from django.apps import apps
apps.get_model('app_name', 'Model')
per your eg:
apps.get_model('people', 'Person')
per:
Import Error :cannot import name get_model
The blessed way to do this in Django 1.7+ is:
import django
model_cls = django.apps.apps.get_model('app_name', 'model_name')
So, in the canonical example of all framework tutorials:
import django
entry_cls = django.apps.apps.get_model('blog', 'entry') # Case insensitive
In case you don't know in which app your model exists, you can search it this way:
from django.contrib.contenttypes.models import ContentType
ct = ContentType.objects.get(model='your_model_name')
model = ct.model_class()
Remember that your_model_name must be lowercase.
Another rendition with less code for the lazy. Tested in Django 2+
from django.apps import apps
model = apps.get_model("appname.ModelName") # e.g "accounts.User"
I'm not sure where it's done in Django, but you could do this.
Mapping the class name to the string via reflection.
classes = [Person,Child,Parent]
def find_class(name):
for clls in classes:
if clls.__class__.__name__ == name:
return clls
Here is a less django-specific approach to get a class from string:
mymodels = ['ModelA', 'ModelB']
model_list = __import__('<appname>.models', fromlist=mymodels)
model_a = getattr(model_list, 'ModelA')
or you can use importlib as shown here:
import importlib
myapp_models = importlib.import_module('<appname>.models')
model_a = getattr(myapp_models, 'ModelA')