I want to add few fields to every model in my django application. This time it's created_at, updated_at and notes. Duplicating code for every of 20+ models seems dumb. So, I decided to use abstract base class which would add these fields. The problem is that fields inherited from abstract base class come first in the field list in admin. Declaring field order for every ModelAdmin class is not an option, it's even more duplicate code than with manual field declaration.
In my final solution, I modified model constructor to reorder fields in _meta before creating new instance:
class MyModel(models.Model):
# Service fields
notes = my_fields.NotesField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
last_fields = ("notes", "created_at", "updated_at")
def __init__(self, *args, **kwargs):
new_order = [f.name for f in self._meta.fields]
for field in self.last_fields:
new_order.remove(field)
new_order.append(field)
self._meta._field_name_cache.sort(key=lambda x: new_order.index(x.name))
super(MyModel, self).__init__(*args, **kwargs)
class ModelA(MyModel):
field1 = models.CharField()
field2 = models.CharField()
#etc ...
It works as intended, but I'm wondering, is there a better way to acheive my goal?
I was having the very same problem, but I found these solutions to be problematic, so here's what I did:
class BaseAdmin(admin.ModelAdmin):
def get_fieldsets(self, request, obj = None):
res = super(BaseAdmin, self).get_fieldsets(request, obj)
# I only need to move one field; change the following
# line to account for more.
res[0][1]['fields'].append(res[0][1]['fields'].pop(0))
return res
Changing the fieldset in the admin makes more sense to me, than changing the fields in the model.
If you mainly need the ordering for Django's admin you could also create your "generic"-admin class via sub-classing Django's admin class. See http://docs.djangoproject.com/en/dev/intro/tutorial02/#customize-the-admin-form for customizing the display of fields in the admin.
You could overwrite the admin's __init__ to setup fields/fieldsets on creation of the admin instance as you wish. E.g. you could do something like:
class MyAdmin(admin.ModelAdmin):
def __init__(self, model, admin_site):
general_fields = ['notes', 'created_at', 'updated_at']
fields = [f.name for f in self.model._meta.fields if f.name not in general_fields]
self.fields = fields + general_fields
super(admin.ModelAdmin, self).__init__(model, admin_site)
Besides that i think it's not a good practice to modify the (private) _field_name_cache!
I ALSO didn't like the other solutions, so I instead just modified the migrations files directly.
Whenever you create a new table in models.py, you will have to run "python manage.py makemigrations" (I believe this in Django >= v1.7.5). Once you do this, open up the newly created migrations file in your_app_path/migrations/ directory and simply move the rows to the order you want them to be in. Then run "python manage.py migrate". Voila! By going into "python manage.py dbshell" you can see that the order of the columns is exactly how you wanted them!
Downside to this method: You have to do this manually for each table you create, but fortunately the overhead is minimal. And this can only be done when you're creating a new table, not to modify an existing one.
Related
I have a parent model which contains a database of unique records, as follows (truncated - there are many more fields):
models.py - parent
class DBPlatform(models.Model):
description = models.CharField(max_length=300, unique=True)
PDS_date = models.DateField()
PDS_version = models.CharField(max_length=50, blank=True)
I use this model to create a child model to save me copying all of the fields. The child model saves specific user-generated instances of the parent records. They are stored separately as they may be edited by the user:
models.py - child
class Platform(DBPlatform):
scenario = models.ForeignKey(Scenario,
on_delete=models.CASCADE,
related_name="platforms")
database_platform = models.ForeignKey(DBPlatform,
on_delete=models.CASCADE,
related_name="instances")
edited = models.BooleanField()
I am using Django REST Framework to create an API for the eventual app. When a child model is created, I want to update all of its inherited fields with those of the parent model. The incredibly convoluted steps I have taken so far (that do not work) are in the views.py file of the child mode. As follows:
api.views.py - child
class PlatformViewSet(viewsets.ModelViewSet):
lookup_field = "id"
serializer_class = PlatformSerializer
permission_classes = [IsAuthenticated]
def perform_create(self, serializer):
db_id = self.request.data["database_platform"]
database_platform = get_object_or_404(DBPlatform, id=db_id)
datadict = self.request.data.dict()
datadict.update(database_platform.__dict__)
query_dict = QueryDict('', mutable=True)
query_dict.update(datadict)
self.request.data = query_dict
serializer.save()
How can I achieve what I am looking to do? I surely am taking the wrong approach as this can't be an uncommon thing.
EDIT:
Ruddra's comment has made me consider that the whole design pattern is faulty. Should I just be using a single model and a boolean flag for the "template" instance?
The serializer data is not changed by the operations before serializer.save().
If you want to do it this way, you'll have edit the serializer or re-serialize the data.
Unless this is something that needs to happen only through API and only on this endpoint, I'd suggest overwriting the model's save method or using pre_save signal. To make sure this operation is performed only when creating a new instance, you can check if self (in case of overwriting save) or instance (in case of signal) has id.
I’ve got these two models (examples) and when I’m trying to run my tests - it errors out saying: no such table: my_app_modelA - if I scroll up I can see that it is bombing out when creating modelB (which I assume is due to the default being applied). Is there a way to order these so that modelA will always get created before modelB? Or should I not be referencing that method as a default attribute? Just trying to get my tests working and this is my sticking point.
My models look like this:
class modelA(models.Model):
attribute = models.IntegerField()
active = models.BooleanField(default=False)
#classmethod
def get_active_attribute(cls):
return modelA.objects.get(active=True).attribute
class modelB(models.Model):
attribute = models.IntegerField(default=modelA.get_active_attribute())
My questions are:
Is that an acceptable thing to do - having default call another model method?
Is there a way to handle the creation of those models in a way that I can guarantee that modelA gets created first so modelB can succesfully create in my tests?
First of all, the migrations happen in the order which is defined when migration file is created.
# 0001_initial.py
...
operations = [
migrations.CreateModel(
name=modelA,
....
),
migrations.CreateModel(
name=modelB,
....
),
]
You can check your migration files and make sure modelA is before modelB.
Secondly, modelA.get_active_attribute() needs a DB entry to be able to return something. While running migrations, you are not inserting data. So you should not be declaring default by other model's object.
You should instead override save() to ensure the default value is based on modelA's attribute.
class modelB(models.Model):
attribute = models.IntegerField()
def save(self, *args, **kwargs):
if self.attribute is None:
self.attribute = modelA.get_active_attribute()
super(modelB, self).save(*args, **kwargs)
How make some fields read-only for particular user permission level?
There is a Django REST API project. There is an Foo serializer with 2 fields - foo and bar. There are 2 permissions - USER and ADMIN.
Serializer is defined as:
class FooSerializer(serializers.ModelSerializer):
...
class Meta:
model = FooModel
fields = ['foo', 'bar']
How does one makes sure 'bar' field is read-only for USER and writable for ADMIN?
I would use smth like:
class FooSerializer(serializers.ModelSerializer):
...
class Meta:
model = FooModel
fields = ['foo', 'bar']
read_only_fields = ['bar']
But how to make it conditional (depending on permission)?
You can use get_serializer_class() method of the view to use different serializers for different users:
class ForUserSerializer(serializers.ModelSerializer):
class Meta:
model = ExampleModel
fields = ('id', 'name', 'bar')
read_only_fields = ('bar',)
class ForAdminSerializer(serializers.ModelSerializer):
class Meta:
model = ExampleModel
fields = ('id', 'name', 'bar', 'for_admin_only_field')
class ExampleView(viewsets.ModelViewSet):
...
def get_serializer_class(self):
if self.request.user.is_admin:
return ForAdminSerializer
return ForUserSerializer
Although Fian's answer does seem to be the most obviously documented way there is an alternative that draws on other documented code and which enables passing arguments to the serializer as it is instantiated.
The first piece of the puzzle is the documentation on dynamically modifying a serializer at the point of instantiation. That documentation doesn't explain how to call this code from a viewset or how to modify the readonly status of fields after they've been initated - but that's not very hard.
The second piece - the get_serializer method is also documented - (just a bit further down the page from get_serializer_class under 'other methods') so it should be safe to rely on (and the source is very simple, which hopefully means less chance of unintended side effects resulting from modification). Check the source under the GenericAPIView (the ModelViewSet - and all the other built in viewset classes it seems - inherit from the GenericAPIView which, defines get_serializer.
Putting the two together you could do something like this:
In a serializers file (for me base_serializers.py):
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Adding this next line to the documented example
read_only_fields = kwargs.pop('read_only_fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# another bit we're adding to documented example, to take care of readonly fields
if read_only_fields is not None:
for f in read_only_fields:
try:
self.fields[f].read_only = True
exceptKeyError:
#not in fields anyway
pass
Then in your viewset you might do something like this:
class MyUserViewSet(viewsets.ModelViewSet):
# ...permissions and all that stuff
def get_serializer(self, *args, **kwargs):
# the next line is taken from the source
kwargs['context'] = self.get_serializer_context()
# ... then whatever logic you want for this class e.g:
if self.request.user.is_staff and self.action == "list":
rofs = ('field_a', 'field_b')
fs = ('field_a', 'field_c')
# add all your further elses, elifs, drawing on info re the actions,
# the user, the instance, anything passed to the method to define your read only fields and fields ...
# and finally instantiate the specific class you want (or you could just
# use get_serializer_class if you've defined it).
# Either way the class you're instantiating should inherit from your DynamicFieldsModelSerializer
kwargs['read_only_fields'] = rofs
kwargs['fields'] = fs
return MyUserSerializer(*args, **kwargs)
And that should be it! Using MyUserViewSet should now instantiate your UserSerializer with the arguments you'd like - and assuming your user serializer inherits from your DynamicFieldsModelSerializer, it should know just what to do.
Perhaps its worth mentioning that of course the DynamicFieldsModelSerializer could easily be adapted to do things like take in a read_only_exceptions list and use it to whitelist rather than blacklist fields (which I tend to do). I also find it useful to set the fields to an empty tuple if its not passed and then just remove the check for None ... and I set my fields definitions on my inheriting Serializers to 'all'. This means no fields that aren't passed when instantiating the serializer survive by accident and I also don't have to compare the serializer invocation with the inheriting serializer class definition to know what's been included...e.g within the init of the DynamicFieldsModelSerializer:
# ....
fields = kwargs.pop('fields', ())
# ...
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# ....
NB If I just wanted two or three classes that mapped to distinct user types and/or I didn't want any specially dynamic serializer behaviour, I might well probably stick with the approach mentioned by Fian.
However, in a case of mine I wanted to adjust the fields based both on the action as well as the admin level of the user making the request, which led to many long and annoying serializer class names. It began to feel ugly creating many serializer classes simply to tweak the list of fields and readonly fields. That approach also meant that the list of fields was separated from the relevant business logic in the view. It might be debatable whether thats a good thing but when the logic gets a tad more involved, I thought it would make the code less, rather than more, maintainable. Of course it makes even more sense to use the approach I've outlined above if you also want to do other 'dynamic' things on the initiation of the serializer.
You can extend the get_fields method in the serializer class. In your case it would look like this:
class FooSerializer(serializers.ModelSerializer):
...
class Meta:
model = FooModel
fields = ["foo", "bar"]
def get_fields(self):
fields = super().get_fields() # Python 3 syntax
request = self.context.get("request", None)
if request and request.user and request.user.is_superuser is False:
fields["bar"].read_only = True
return fields
I'm trying to create editable set of objects.
I have Visitor model, which can contain set of models Sibling. But the set may be blank. This set should be editable in Django admin, and, I would like it will be generated by built-in tools.
Here is my approach to do this:
class Sibling(models.Model):
VisitorID = models.ForeignKey('Visitor')
# ... some fields
class Visitor(models.Model):
# ... some fields
Siblings = models.ManyToManyField(Sibling, blank=True)
It is bad way because there are all Siblings from all Visitors in the auto-generated form in django admin, but I want only those which are related to specific Visitor.
Could anyone help me or give advice?
One way achieve this with a Serializer class
class Sibling(models.Model):
VisitorID = models.ForeignKey('Visitor')
# ... some fields
class Visitor(models.Model):
# ... some fields
class VisitorSerializer(serializers.ModelSerializer):
sibling = serializers.RelatedField(source='sibling')
class Meta:
model = Visitor
# List all fields in Visitor plus sibling
fields = ('id', 'somefieldinvisitormodel', 'sibling')
The serializer class allows you to override what's being displayed in admin without having to mess with your models. This also allows you to remove the extra relationship you added within visitor.
This isn't an entirely automated solution, but it's close.
I am using Python 2.7 and Django 1.6.3
I want to define extra model field which is not actually in db table. I have a way which is defining a callable method with property annotation like;
class MyClass(models.Model):
my_field = models.CharField(max_length=50)
#property
def my_extra_field(self):
return self.my_field+'extra value'
This works fine to show it on admin change list pages. But the extra field is not on db level. It is being generated on programming level. Django asks it for every model object.
This cause me some troubles. My all admin change list pages have capability of exporting as excel or some other type. I am using admin query set to build that report. I have also jasper reports mechanism that works with SQL select queries. So, I, want to use the queryset to take this select query.
I think being able to define extra fields on db level is important for something. Not just for reason of mine. So, the question all about this.
Is there a way to define an extra custom fields on db level instead of programming level in Django.
Thank you!.
Edited
Adding it to admin list_filter is also another problem if it is not really a field. Django does not allow you to add it.
Could you create a new database field and then overwrite the save method to populate that field? I do that often to create a marked up version of a text field. For example:
class Dummmy(models.Model):
content = models.TextField()
content_html = models.TextField(editable=False, null=True)
def save(self, *args, **kwargs):
self.content_html = markdown(self.content)
super(Dummmy, self).save(*args, **kwargs)
So for you:
class MyClass(models.Model):
my_field = models.CharField(max_length=50)
my_extra_field = models.CharField(editable=False, null=True)
def save(self, *args, **kwargs):
self.my_extra_field = self.my_field + 'extra value'
super(MyClass, self).save(*args, **kwargs)