Django: Get model from string? - django

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

Related

Django model field dynamic choices which calls API

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',
]

How to make import-export save JSONField not as string

I'm trying to import JSONField using django import-export, it keeps saving JSON as string (adding "" to it)
models.py
from django.db import models
from django.contrib.postgres.fields import JSONField
class Governorate(models.Model):
name = models.CharField(max_length=500)
data = JSONField()
def __str__(self):
return ("%s" %(self.name))
admin.py
from django.contrib import admin
from .models import Governorate
from import_export.admin import ImportExportModelAdmin
from import_export import resources
class GovernorateResource(resources.ModelResource):
class Meta:
model = Governorate
class GovernorateAdmin(ImportExportModelAdmin):
list_display = ('id','name', 'data')
resources_class = GovernorateResource
admin.site.register(Governorate,GovernorateAdmin)
I expected the output to be: {"xx":{"xx":"xx","xx":"xx"} however it saves it as "{"xx":{"xx":"xx","xx":"xx"}"
Tried uploading XLSX and CSV.
Version 1.2.0 of import-export doesn't automatically recognise the JSONField in your Resource so it just defaults to a CharField type. It's already added to the master branch but not released.
So just override the field to use the JSONWidget which is already available in version 1.2.0:
from import_export import fields, widgets
class GovernorateResource(resources.ModelResource):
data = fields.Field(widget=widgets.JSONWidget())
class Meta:
model = Governorate
So basically, the import-export library was saving the JSONField() as a string. So my solution was to create a signal to check if the instance was a string or a dict. then fix it.
I tried to create a Field with the JSONWidget widget, but doesn't work.
models.py
#receiver(post_save, sender=Governorate)
def fix_json(sender, instance, **kwargs):
if (type(instance.data) is str):
instance.data = eval(instance.data)
instance.save()
print(instance.data)
The Django import-export library is really good, but lacks proper documentation to be honest.

how to import models to oscar.partner app's strategy module correctly?

I have defined declared my own Strategy to Select a Stock and a price as described in http://django-oscar.readthedocs.io/en/releases-1.1/topics/prices_and_availability.html
Everything worked fine until I had the need to import a custom model class that I created in the catalogue app.
My goal was to access this custom model for the price selection strategy.
in /apps/partner/strategy I tried to import the model like this:
CountrySpecificProductInformation = get_model('catalogue', 'CountrySpecificProductInformation')
this call raises a Model not Registered exception:
File "/home/matyas/virtenvs/oscar/local/lib/python2.7/site-packages/oscar/core/loading.py", line 250, in get_model
return apps.get_registered_model(app_label, model_name)
File "/home/matyas/virtenvs/oscar/local/lib/python2.7/site-packages/django/apps/registry.py", line 260, in get_registered_model
"Model '%s.%s' not registered." % (app_label, model_name))
LookupError: Model 'catalogue.CountrySpecificProductInformation' not registered.
my Installed apps settings look like this:
INSTALLED_APPS = ['...'] +
oscar.get_core_apps(['apps.catalogue', 'apps.promotions', 'apps.dashboard',
'apps.dashboard.catalogue', 'apps.partner', 'apps.payment', 'apps.dashboard.partners',
'apps.shipping', 'apps.checkout', 'apps.search'])
I am using django-oscar 1.3 with Django 1.9.9
Oscar has its own importing, system that is necessary to be able to overwrite any part of any app of the e-commerce solution.
The strategy class gets imported early on the when the server starts, and some models are not registered at that time yet.
The solution was to import the module not at the top of the module in the import section, but instaed only in the method where the models are necessary. (In my case that was the the select_stockrecord method:
from oscar.core.loading import get_class, get_model
CountrySpecificProductInformation = get_model('catalogue',
'CountrySpecificProductInformation')
def select_stockrecord(self, product):
CountrySpecificProductInformation = get_model('catalogue', 'CountrySpecificProductInformation')
Country = get_model('catalogue', 'Country')
It is not a perfect solution but I prefer this rather than writing raw sql queries directly to the database.

When verbose_name changes, how do I auto-update a model's ContentType?

Given is a Django model called BlogPost. At first, it's coded without a Meta.verbose_name. At ./manage.py syncdb time, a ContentType with a name "blog post" is created automatically. At some later point of time, Meta.verbose_name of "Blog post" is added.
Now there is a discrepancy: ContentType is called "blog post", while the model goes by verbose name of "Blog post", this difference is shown in any framework using generic relationships, e.g. in comments' admin. I would like to correct this situation by changing the name of the ContentType, however, I wouldn't want to do that either by hand (for obvious reasons) or via a migration (since I don't migrate anything else, Meta.verbose_name is just a code change).
How would you update the ContentType's name upon Meta.verbose_name change?
Answering own question: I've managed to do this with a small post_migrate signal. If you are not using South, it's probably perfectly possible to use the post_syncdb signal the same way. Any comments on this code are appreciated.
from django.contrib.contenttypes.models import ContentType
from django.utils.functional import Promise
from south.signals import post_migrate
# or if using django >=1.7 migrations:
# from django.db.models.signals import post_migrate
def update_contenttypes_names(**kwargs):
for c in ContentType.objects.all():
cl = c.model_class()
# Promises classes are from translated, mostly django-internal models. ignore them.
if cl and not isinstance(cl._meta.verbose_name, Promise):
new_name = cl._meta.verbose_name
if c.name != new_name:
print u"Updating ContentType's name: '%s' -> '%s'" % (c.name, new_name)
c.name = new_name
c.save()
post_migrate.connect(update_contenttypes_names, dispatch_uid="update_contenttypes")
Another approach is to override ContentType.__str__ method, as it looks like this:
def __str__(self):
# self.name is deprecated in favor of using model's verbose_name, which
# can be translated. Formal deprecation is delayed until we have DB
# migration to be able to remove the field from the database along with
# the attribute.
#
# We return self.name only when users have changed its value from the
# initial verbose_name_raw and might rely on it.
model = self.model_class()
if not model or self.name != model._meta.verbose_name_raw:
return self.name
else:
return force_unicode(model._meta.verbose_name)
So, you can rewrite it, if you don't need some kind of backwards compatibility:
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import force_unicode
def contenttype_as_str(self):
return force_unicode(self.model_class()._meta.verbose_name)
ContentType.__str__ = contenttype_as_str
It's a bit tricky, but I believe it's more straightforward. Note, that since Django 1.4.1 force_text is used instead of force_unicode.

Django custom field validator vs. clean

I would like to create TodayOrLaterDateField() which would subclass DateField() field as I am using this condition in many places. The purpose of this field would be avoiding putting dates from the past.
What is the most straightway way of doing this? I am confused with validator vs. clean method.
I've tried with clean() but when comparing value to datetime.date.today() I am getting "compare unicode object to date" error.
I'm using Django 1.3
Validators only validate, they don't return the improved format;
Clean methods both validate and return a (sometimes amended) value.
I think the way to go here is to just use a DateField with a validator as a inherited class of DateField with a default_validators set.
import datetime
from django.core import exceptions
from django.db import models
from django.utils.translation import ugettext_lazy as _
def validate_date_today_or_later(value):
'Place this in validators.py and import it to keep your model a bit cleaner'
if value < datetime.date.today():
raise exceptions.ValidationError(_('Date must be today or later'))
class TodayOrLaterDateField(models.DateField):
default_validators = [validate_date_today_or_later,]
edit:
You can apply the same validator to your form fields as well if you just want it there and not in your whole app.
You can extend models.DateField and override to_python method. Didn't tested on Django 1.3 but should work.
import datetime
from django.core import exceptions
from django.db import models
class TodayOrLaterDateField(models.DateField):
def to_python(self, value):
value = super(TodayOrLaterDateField, self).to_python(value)
if value < datetime.date.today():
raise exceptions.ValidationError(u'Date must be today or later')
return value