I have 20+ MySQL tables, prm_a, prm_b, ... with the same basic structure but different names, and I'd like to associate them with Django model classes without writing each one by hand. So, feeling ambitious, I thought I'd try my hand at using type() as a class-factory:
The following works:
def get_model_meta_class(prm_name):
class Meta:
app_label = 'myapp'
setattr(Meta, 'db_table', 'prm_%s' % prm_name)
return Meta
prm_class_attrs = {
'foo': models.ForeignKey(Foo),
'val': models.FloatField(),
'err': models.FloatField(blank=True, null=True),
'source': models.ForeignKey(Source),
'__module__': __name__,
}
###
prm_a_attrs = prm_class_attrs.copy()
prm_a_attrs['Meta'] = get_model_meta_class('a')
Prm_a = type('Prm_a', (models.Model,), prm_a_attrs)
prm_b_attrs = prm_class_attrs.copy()
prm_b_attrs['Meta'] = get_model_meta_class('b')
Prm_b = type('Prm_b', (models.Model,), prm_b_attrs)
###
But if I try to generate the model classes as follows:
###
prms = ['a', 'b']
for prm_name in prms:
prm_class_name = 'Prm_%s' % prm_name
prm_class = type(prm_class_name, (models.Model,), prm_class_attrs)
setattr(prm_class, 'Meta', get_model_meta_class(prm_name))
globals()[prm_class_name] = prm_class
###
I get a curious Exception on the type() line (given that __module__ is, in fact, in the prm_class_attrs dictionary):
File ".../models.py", line 168, in <module>
prm_class = type(prm_class_name, (models.Model,), prm_class_attrs)
File ".../lib/python2.7/site-packages/django/db/models/base.py", line 79, in __new__
module = attrs.pop('__module__')
KeyError: u'__module__'
So I have two questions: what's wrong with my second approach, and is this even the right way to go about creating my class models?
OK - thanks to #Anentropic, I see that the items in my prm_class_attrs dictionary are being popped away by Python when it makes the classes. And I now have it working, but only if I do this:
attrs = prm_class_attrs.copy()
attrs['Meta'] = get_model_meta_class(prm_name)
prm_class = type(prm_class_name, (models.Model,), attrs)
not if I set the Meta class as an attribtue with
setattr(prm_class, 'Meta', get_model_meta_class(prm_name))
I don't really know why this is, but at least I have it working now.
The imediate reason is because you are not doing prm_class_attrs.copy() in your for loop, so the __modules__ key is getting popped out of the dict on the first iteration
As for why this doesn't work:
setattr(prm_class, 'Meta', get_model_meta_class(prm_name))
...it's to do with the fact that Django's models.Model has a metaclass. But this is a Python metaclass which customises the creation of the model class and is nothing to do with the Meta inner-class of the Django model (which just provides 'meta' information about the model).
In fact, despite how it looks when you define the class in your models.py, the resulting class does not have a Meta attribute:
class MyModel(models.Model):
class Meta:
verbose_name = 'WTF'
>>> MyModel.Meta
AttributeError: type object 'MyModel' has no attribute 'Meta'
(You can access the Meta class directly, but aliased as MyModel._meta)
The model you define in models.py is really more of a template for a model class than the actual model class. This is why when you access a field attribute on a model instance you get the value of that field, not the field object itself.
Django model inheritance can simplify a bit what you're doing:
class GeneratedModelBase(models.Model):
class Meta:
abstract = True
app_label = 'myapp'
foo = models.ForeignKey(Foo)
val = models.FloatField()
err = models.FloatField(blank=True, null=True)
source = models.ForeignKey(Source)
def generate_model(suffix):
prm_class_name = 'Prm_%s' % prm_name
prm_class = type(
prm_class_name,
(GeneratedModelBase,),
{
# this will get merged with the attrs from GeneratedModelBase.Meta
'Meta': {'db_table', 'prm_%s' % prm_name},
'__module__': __name__,
}
)
globals()[prm_class_name] = prm_class
return prm_class
prms = ['a', 'b']
for prm_name in prms:
generate_model(prm_name)
You can use ./manage.py inspectdb
This will print out a python models file for the DB you're pointing at in your settings.py
Documentation
EDIT
For dynamic models, try django-mutant or check out this link
For anyone still wondering how to do this, I took the genius answer from #Anentropic and made it work with some modifications.
I also changed the db table name to something more like django uses to name the tables of the models ("appname_lowercaseclassname") by stripping all non alphabetic characters from the class name and converting the resulting string to lower case.
I Tested it on Django 2.2.6
def generate_model(class_name):
clean_name_for_table = ''.join(filter(str.isalpha, class_name)).lower() # Could also use regex to remove all non alphabetic characters.
db_table_name = f"{__package__}_{clean_name_for_table}"
prm_class = type(
class_name,
(BaseClass,),
{
# this will get merged with the attrs from GeneratedModelBase.Meta
'Meta': type("Meta",(),{'db_table':db_table_name}),
'__module__': __name__,
}
)
globals()[class_name] = prm_class
return prm_class
Related
I'm trying to build two abstract classes called SurveyQuestionBase and SurveyResponseBase that will serve as templates to quickly define new concrete Models for implementing specific surveys on our website. The issue I am having is in enforcing that the SurveyResponseBase model, when made concrete, should define a ForeignKey to a concrete model of SurveyQuestionBase.
Django does not allow us to define ForeignKeys to abstract classes so I cannot, for instance, do this:
question = models.ForeignKey(SurveyQuestionBase)
Neither can I have it as None or app_label.ModelName for similar reasons.
One hacky fix is to create a new concrete model SurveyQuestionConcrete and make the ForeignKey point to this: question = models.ForeignKey(concrete_model), combined with validation to ensure this model is replaced.
Is there a cleaner way to achieve the same thing? All I need to do is ensure that when someone defines a concrete model from SurveyResponseBase they include a ForeignKey to a concrete model defined from SurveyQuestionBase
Here's the full code:
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
# Implementation borrows from: https://github.com/jessykate/django-survey/
class SurveyQuestionBase(models.Model):
TEXT = 'text'
INTEGER = 'integer'
RADIO = 'radio'
SELECT = 'select'
MULTI_SELECT = 'multi-select'
ANSWER_TYPE_CHOICES = (
(INTEGER, 'Integer',),
(TEXT, 'Text',),
(RADIO, 'Radio',),
(SELECT, 'Select',),
(MULTI_SELECT, 'Multi-Select',),
)
question = models.TextField()
required = models.BooleanField()
question_type = models.CharField(choices=ANSWER_TYPE_CHOICES, max_length=20)
class Meta:
abstract = True
class SurveyResponseBase(models.Model):
"""
concrete_question_model: 'app_label.Model' - Define the concrete model this question belongs to
"""
concrete_model = 'SurveyQuestionBase'
question = models.ForeignKey(concrete_model)
response = models.TextField()
class Meta:
abstract = True
Two solutions (both working) to this problem:
The first solution involves using GenericForeignKey. The second is more interesting and involves generating the SurveyResponseBase dynamically.
Solution 1: Using GenericForeignKey
class SurveyQuestionBase(models.Model):
TEXT = 'text'
INTEGER = 'integer'
RADIO = 'radio'
SELECT = 'select'
MULTI_SELECT = 'multi-select'
ANSWER_TYPE_CHOICES = (
(INTEGER, 'Integer',),
(TEXT, 'Text',),
(RADIO, 'Radio',),
(SELECT, 'Select',),
(MULTI_SELECT, 'Multi-Select',),
)
question = models.TextField()
required = models.BooleanField()
question_type = models.CharField(choices=ANSWER_TYPE_CHOICES, max_length=20)
class Meta:
abstract = True
#classmethod
def get_subclasses(cls, *args, **kwargs):
for app_config in apps.get_app_configs():
for app_model in app_config.get_models():
model_classes = [c.__name__ for c in inspect.getmro(app_model)]
if cls.__name__ in model_classes:
yield app_model
class SurveyResponseBase(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, limit_choices_to=get_content_choices)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
response = models.TextField()
class Meta:
abstract = True
def get_content_choices():
query_filter = None
for cls in SurveyQuestionBase.get_subclasses():
app_label, model = cls._meta.label_lower.split('.')
current_filter = models.Q(app_label=app_label, model=model)
if query_filter is None:
query_filter = current_filter
else:
query_filter |= current_filter
return query_filter
Solution 2: Dynamic base class generation
class SurveyQuestionBase(models.Model):
TEXT = 'text'
INTEGER = 'integer'
RADIO = 'radio'
RATING = 'rating'
SELECT = 'select'
MULTI_SELECT = 'multi-select'
QUESTION_TYPES = (
(INTEGER, 'Integer'),
(TEXT, 'Text'),
(RADIO, 'Radio'),
(RATING, 'Rating'),
(SELECT, 'Select'),
(MULTI_SELECT, 'Multi-Select'),
)
CHOICE_TYPES = (RADIO, RATING, SELECT, MULTI_SELECT)
question = models.TextField()
required = models.BooleanField()
question_type = models.CharField(choices=QUESTION_TYPES, max_length=20)
choices = models.TextField(blank=True, null=True)
choices.help_text = """
If the question type is "Radio," "Select," or "Multi-Select",
provide a comma-separated list of options for this question
"""
class Meta:
abstract = True
Meta = type('Meta', (object,), {'abstract': True})
def get_response_base_class(concrete_question_model):
"""
Builder method that returns the SurveyResponseBase base class
Args:
concrete_question_model: Concrete Model for SurveyQuestionBase
Returns: SurveyResponseBase Class
"""
try:
assert SurveyQuestionBase in concrete_question_model.__bases__
except AssertionError:
raise ValidationError('{} is not a subclass of SurveyQuestionBase'.format(concrete_question_model))
attrs = {
'question': models.ForeignKey(concrete_question_model, related_name='responses'),
'response': models.TextField(),
'__module__': 'survey_builder.models',
'Meta': Meta(),
}
return type('SurveyResponseBase', (models.Model,), attrs)
We decided to go ahead with Solution 2 since the GenericForeignKeys approach requires an additional ContentType selection.
I believe you can't do that because the ForeignKey doesn't know what actual model to point to.
You may be looking for GenericForeignKey (https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#generic-relations). It allows you to define that relationship properly.
I have defined a serializer like this:
class ActivitySerializer(serializers.ModelSerializer):
activity_project = serializers.SlugRelatedField(queryset=Project.objects.all(), slug_field='project_name')
activity_tags = serializers.SlugRelatedField(queryset=Tag.objects.all(), slug_field='tag_name', many=True)
class Meta:
model = Activity
fields = ('activity_name', 'activity_description', 'activity_status', 'activity_completion_percent', 'activity_due_date', 'activity_project', 'activity_tags',)
Now if I insert an activity_tag that does not exist in the database, I get a validation error"
{
"activity_tags": [
"Object with tag_name=test does not exist."
]
}
I would like to create a validation method that adds the tag in the database if it does not exist.
I have tried using the
def validate(self, attrs):
....
method, but apparently for a slug field there is a method that is called before this one.
Can someone point me to the right method I should use? Would this method be called in the corresponding view?
I think you would need to create a nested serializer for this to work. This is totally untested and off the top of my head, but maybe something like this:
class ActivityTagFieldSerializer(serializer.ModelSerializer):
tag_name = serializers.SlugField()
class Meta:
model = Tag
fields = ('tag_name')
class ActivitySerializer(serializer.ModelSerializer):
activity_tags = ActivityTagFieldSerializer(many=True)
class Meta:
model = Activity
fields = ('activity_tags', 'activity_project', ...)
def create(self, validated_data):
tags = validated_data.pop('activity_tags')
activity = Activity.objects.create(**validated_data)
for tag in tags:
try:
tag_to_add = Tag.objects.get(**tag)
except:
tag_to_add = Tag.objects.create(**tag)
activity.activity_tags.add(tag_to_add)
return activity
Check the API guide for writable nested serializers
I managed to do this by subclassing SlugRelatedField and overriding "to_internal_value" method. In the original implementation this method tries to get an object from the queryset, and if an object doesn't exist it fails the validation. So instead of calling "get" method, I'm calling "get_or_create":
class CustomSlugRelatedField(serializers.SlugRelatedField):
def to_internal_value(self, data):
try:
obj, created = self.get_queryset().get_or_create(**{self.slug_field: data})
return obj
except (TypeError, ValueError):
self.fail('invalid')
So I have a base ItemTable, and then a number of Tables that inherit from it. I don't seem to be able to modify the Meta class. I tried just including the meta class normally and it didn't work, then I found this bug report and implemented it below. It fails silently: the tables render only with the columns from the parent meta class.
class ItemTable(tables.Table):
class Meta:
model = Item
attrs = {"class":"paleblue"}
fields = ('name', 'primary_tech', 'primary_biz', 'backup_tech', 'backup_biz')
class ApplicationTable(ItemTable):
def __init__(self, *args, **kwargs):
super(ApplicationTable, self).__init__(*args, **kwargs)
class Meta(ItemTable.Meta):
model = Application
fields += ('jira_bucket_name',)
EDIT: Code amended as shown. I now get a NameError that fields is not defined.
Try:
class ApplicationTable(ItemTable):
class Meta:
model = Application
fields = ItemTable.Meta.fields + ('jira_bucket_name',)
You'll have the same problems extending Meta in a table, as you will in a normal Django model.
You didnt add , (comma) to one-element tuple. Try to change this line Meta.attrs['fields'] += ('jira_bucket_name') in ApplicationTable to:
Meta.attrs['fields'] += ('jira_bucket_name',)
if it didnt help try to create Meta class outsite model class definition:
class ItemTableMeta:
model = Item
attrs = {"class":"paleblue"}
fields = ('name', 'primary_tech', 'primary_biz', 'backup_tech', 'backup_biz')
class ApplicationTableMeta(ItemTableMeta):
model = Application
fields = ItemTableMeta.fields + ('jira_bucket_name',)
class ItemTable(tables.Table):
#...
Meta = ItemTableMeta
class ApplicationTable(ItemTable):
#...
Meta = ApplicationTableMeta
You may need to take this up with the django-tables author. This is not a problem with standard Django.
Is it possible to override values inside a Model?
I am getting 'MyModel' object does not support item assignment.
my_model = MyModel.objects.get(id=1)
print my_model.title
if my_model.is_changed:
my_model['title'] = 'something' # 'MyModel' object does not support item assignment
params = {
'my_model': my_model,
...
}
return render(request, 'template.html', params)
Models are objects, not dictionaries. Set attributes on them directly:
if my_model.is_changed:
my_model.title = 'something'
Or, if the attribute name is dynamic, use setattr:
attr_name = 'title' # in practice this would be more complex
if my_model.is_changed:
setattr(my_model, attr_name, 'something')
This changes the in-memory copy of the model, but makes no database changes - for that your attribute would have to be a field and you'd have the call the save method on my_model. You don't need to do that if you just want to change what the template receives in its context, but just for completeness's sake:
if my_model.is_changed:
my_model.title = 'something'
my_model.save()
Dictionaries are mutable, if you actually have a dictionary:
mydict = {'title': 'foo'}
# legal
mydict['title'] = 'something'
But not everything is a dictionary.
Yes, you can change values, but this is not how its done. Django Models are Python classes that have models to represent fields. For example a CharField is for holding a string in a database. Let me demonstrate (code from django docs):
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
As you can see above the Python class is a custom Django model. It is linked to a databse, and when you run manage.py syncdb, it will interact with your database to create the tables and columns that you need it to.
Now, in your case:
if my_model.is_changed:
my_model.title = "Something"
my_model.save()
my_model is an object. So, try this:
if my_model.is_changed:
my_model.title = 'something'
my_model.save()
I was using inlineformset_factory, what I had to do was:
Instead of using my_model.title = 'something',
I had to use my_model.instance.title = 'something'
views.py
...
if request.method == "POST":
formset = modelName2(request.POST, instance=modelName1)
if formset.is_valid():
if changeInstance == True:
models = formset
# change the title if changeInstance is True
index = 0
model = models[index]
model.instance.title = "something else"
model.save()
...
My models file works just fine. As soon as I replace every models.Model with MyModel (a child-class of models.Model), one of my models raises a
<class 'puppy.cms.models.Appearance'> has more than 1 ForeignKey to <class 'puppy.cms.models.Segment'>
exception. The only thing that I am doing in the child class is override the clean method.
What could I be doing wrong?
class SansHashUrl(object):
""" Upon each call to clean, iterates over every field,
and deletes all '#/' and '#!/' occurances.
IMPORTANT: This mixin must be listed first in the inheritance list to work
properly. """
def clean(self):
attrs = (field.attname for field in self.__class__._meta.fields
if isinstance(field, models.CharField)
or isinstance(field, models.TextField))
for attr in attrs:
attr_value = self.__getattribute__(attr)
tokens = attr_value.split()
for i, token in enumerate(tokens):
if has_internal_domain(token):
suggested_url = re.sub('#!?/','', token)
tokens[i] = suggested_url
self.__setattr__(attr, ' '.join(tokens))
class MyModel(SansHashUrl, models.Model):
pass
Model that throws the error:
class Appearance(MyModel):
appearance_type = models.CharField(max_length=20,
choices=APPEARANCE_TYPE_CHOICES)
person = models.ForeignKey(Person, related_name='person_appearance')
item = models.ForeignKey(ManagedItem)
class Meta:
unique_together = (('person', 'item'),)
def __unicode__(self):
return self.person.__unicode__()
In reference to:
class Segment(Story, HasStatsTags, HasFullUrl):
...
It might be useful to note that Story is a subclass of ManagedItem (a subclass of MyModel).
You need to declare MyModel (and probably ManagedItem) as an abstract model in its Meta class, otherwise Django will create a separate table for them and define FKs between them.
class MyModel(SansHashUrl, models.Model):
class Meta:
abstract = True