Django - generic ModelAdmin definition - python

This is a newbie question.
I have multiple models that I would like to show in admin view. I would like to show all model's fields when viewing a list of records for a model.
I'd like to define a generic ModelAdmin subclass which will be able to list out all fields for a model. But not sure how to pass the class objects to it. admin.py looks like this
class ModelFieldsAdmin(admin.ModelAdmin):
def __init__(self, classObj):
self.classObj = classObj
self.list_display = [field.name for field in classObj._meta.get_fields()]
admin.site.register(Candy, ModelFieldsAdmin)
admin.site.register(Bagel, ModelFieldsAdmin)
How can I pass the classObj (which would be one of the models Candy or Bagel) , and also ensure that the get_list_display() picks up the value from self.list_display ?

This isn't the right approach. You shouldn't override __init__ for an admin class.
Rather, define get_list_display and access self.model:
def get_list_display(request):
return [field.name for field in self.model._meta.get_fields()]

Related

Django forms.Form reflecting property field constraints

I'm implementing a Django Form witch should contain the field 'fieldA' from modelA:
class ModelA(models.Model):
fieldA = models.CharField(max_length=8)
...
My question is: Is there a way to have Django form, which will automatically handle validation of fieldA (check the max_length)? I know I Could use form.ModelFormclass referring to ModelA, but then the form would reflect all the fields of the ModelA. I would like to use simple forms.Form.
I'm looking for a solution like:
class formX(forms.Form):
fieldA = forms.CharField(**modelA.fieldA.constraints)
fieldB = ... some other fields not related to ModelA ...
.... even more fields
Maybe this question is an XY problem, but let me try...
Direct question: get field constraints from existing model
from django import forms
from django.db import models
class Foo(models.Model):
x = models.CharField(max_length=30)
y = models.IntegerField(null=True)
class FooForm(forms.Form):
foo_x = forms.CharField(max_length=Foo._meta.get_field('x').max_length)
You can access the field directly in two ways:
ModelClass.field_name.field (kind of hack, ModelClass.field_name is a django.db.models.query_utils.DeferredAttribute)
ModelClass._meta.get_field('field_name') (better way, described somewhere in docs)
However, this way you have to a) update form if field constraints are added or b) specify all attributes in advance (max_length, min_length, verbose_name, blank, etc.), making declaration of FooForm.foo_x too verbose.
Alternatives
Fields subset
First of all, if you need a subset of Foo fields, use ModelForm and specify them:
class FooForm(forms.ModelForm):
class Meta:
fields = ('x',)
Now you have a form with only x field.
Add some fields
If you want to add fields to this form (that are not related to other model), do it:
class FooForm(forms.ModelForm):
another_field = forms.IntegerField()
class Meta:
fields = ('x',)
def clean_another_field(self):
data = self.cleaned_data['another_field']
if data != 42:
raise ValidationError('Not an answer!') # i18n should be here
return data
Also you can override clean and save methods for another goals, documentation explains that nicely.
Mix fields from different models
If you need fields of two different models to be present in one form, you don't. You need two separate forms in this case, plus some inter-validation logic outside of this forms maybe (as a view method, for example, or as another class that is not a form). Maybe what you need is inline formset, it doesn't represent two mixed forms, but at least has some inter-model communication.
I found a way how to achieve a validation of form Field reflecting constrains from model Field.
class Foo(models.Model):
x = models.CharField(max_length=30)
y = models.IntegerField(null=True)
class FooForm(forms.Form):
foo_x = forms.CharField(validators=Foo._meta.get_field('x').validators)
Like this, the form will respect the max_length validator of attribute x or any other validator defined.

Django dynamic filterset

So I want to create a django filters.FilterSet from django-filter module, but I want to dynamically add its attributes. For example, if I wanted to add SubName dynamically:
class UsersInfoFilter(filters.FilterSet):
Name=NumberFilter(lookup_type='gte')
def __new__(self):
self.SubName=NumberFilter(lookup_type='gte')
self.Meta.fields.append('SubName')
class Meta:
model = UsersInfo
fields = ['UserID', 'LanguageID', 'Name']
The problem is that FilterSet is a metaclass that immediately runs once the class has been figured out, so there is nowhere before that point that items can be dynamically added.
I've tried putting a function in as a parameter around filters.FilterSet class UsersInfo(AddObjects(filters.FilterSet)) which returns exactly what is passes, but I cannot reference UsersInfoFilter at that point since it still isn't finished being created.
I also tried making UsersInfoFilter its own base class, and then creating a class RealUsersInfoFilter(UsersInfoFilter, filters.FilterSet) as my actual filter, but then FilterSet just throws warnings about missing attributes named as fields.
There doesn't seem to be any kind of constructor function for classes in python. I'm assuming I have to do some kind of magic with metaclasses, but I've tried every combination I can think of and am at wits end.
You can't change Meta subclass from the __init__ method... there are 2 options to approach your issue...
First one - define "wide" filter on all of the model fields:
class UsersInfoFilter(filters.FilterSet):
class Meta:
model = UsersInfo
It will create default filters for all your model fields.
Second, define dynamic fields:
class UsersInfoFilter(filters.FilterSet):
name = NumberFilter(lookup_type='gte')
def __init__(self):
super(UsersInfoFilter, self).__init__()
base_filters['subname'] = NumberFilter(name='subname', lookup_type='gte')
class Meta:
model = UsersInfo
fields = ['user_id', 'language_id', 'name']
(I do not know if this is something you really want - because despite "dynamic" adding field - it should be declared as static - there are no logic here)
p.s.
why CamelCase on properties and fields? use proper pep-8.
To dynamically choose the fields in the FilterSet, I suggest to create a FilterSet factory like this:
def filterset_factory(model, fields):
meta = type(str('Meta'), (object,), {'model': model, 'fields': fields})
filterset = type(str('%sFilterSet' % model._meta.object_name),
(FilterSet,), {'Meta': meta})
return filterset
And then use it like:
DynamicFilterClass = filterset_factory(model=MyModel, fields=[...])
dynamic_filter = DynamicFilterClass(request.GET, queryset=instances)

Use get_object() and aggregate() in DetailView in Django

I have a DetailView in Django. In my template I'm printing the fields in my object dictionary, e.g. object.full_name.
But I also want some aggregates belonging to the object. If say I have a model Person and a model Group and the model Group has a foreign key to Person, can I then override get_object() in my DetailView to be something like:
def get_object(self):
return super(DetailView, self).get_object().aggregate(num_groups=Count('group_set'))
I've tested my suggestion but it doesn't work. I guess there must be some work-around to do this without having to override get_context_data() and populating variables as context['num_groups'] = Group.objects.filter(person=self.object).count(). This could be done but if I have more than one model with a foreign key to Person it would be smarter if I could just accomplish this with an aggregate as in my code example.
DetailView.get_object() returns model instance instance not the queryset. But you can populate any attribute of the object:
def get_object(self):
object = super(PersonView, self).get_object()
object.num_groups = object.group_set.all().count()
return object
Note the PersonView in the super() call. You should use the name of your class view here.
If you want get multiple counts related to person then you can do it with something like this:
counts = Person.objects.filter(pk=object.pk).aggregate(
num_groups=Count('group_set'), num_items=Count('item_set'))
object.num_groups = counts['num_groups']
object.num_items = counts['num_items']

Django: programatic fields in inlines

Expanding from this question: Can "list_display" in a Django ModelAdmin display attributes of ForeignKey fields?, could it be possible to do something like this:
class MyModelInline(admin.StackedInline):
model = MyModel
extra = 1
fields = ('my_field',)
def my_field(self, obj):
return obj.one_to_one_link.my_field
If something like this were possible, it would solve most of my current Django problems, but the code above does not work: Django (rightly) complains that my_field is not present in the form.
You can do that, but you must also add my_field to your MyModelInline class's readonly_fields attribute.
fields = ('my_field',)
readonly_fields = ('my_field',)
From the docs:
The fields option, unlike list_display, may only contain names of fields on the model or the form specified by form. It may contain callables only if they are listed in readonly_fields.
If you need the field to be editable, you should be able to do that with a custom form but it takes more work to process it.

Django Different form validation when adding and changing instance

My question is that can we perform different form validation on the same form based on different action, such as adding or changing.
Say I have a model which has a field named 'type'. And 'type' is an enum field whose choices are 'Manual' and 'Auto'. When adding a new instance, I don't want the user able to set 'type' to 'Auto'. However, the user would view and modify the instance whose 'type' is 'Auto'.
Maybe I don't need validation to do this. So if there is any other option, I' like to hear.
Thanks in advance.
Given the model:
class Spam(models.Model):
foo_type = models.WhateverFieldType(editable=False, choices=FOO_TYPE_CHOICES)
# ... other fields
Use this form (which excludes the foo_type field (disallows editing that field using the form and also disables validation of the field - it's basically, "here, edit anything on this Spam instance except foo_type"):
class SpamForm(forms.ModelForm):
class Meta:
exclude = ('foo_type',)
Then, create a subclass of UpdateView and one of ListView and update the queryset to include only users who's tye_type is set to Auto.
class SpamUpdateView(UpdateView):
def get_queryset(self):
return super(SpamUpdateView, self).get_queryset().filter(foo_type=FOO_TYPE_AUTO)
class SpamListView(ListView):
def get_queryset(self):
return super(SpamListView, self).get_queryset().filter(foo_type=FOO_TYPE_AUTO)

Categories