Retrieving model class dynamically in CBV - django

What is the proper way to retrieve the model class dynamically in a CBV?
I realize I have to use apps.get_model, but not sure where to do that.
I would like to make my delete (and other) views more "DRY".
class DeleteParamView(generic.DeleteView):
# The following does not work since kwargs cannot be accessed
#model = apps.get_model('patients', 'param' + self.kwargs['param_name'])
def __init__(self, *args, **kwargs):
from django.apps import apps
self.model = apps.get_model('persons', 'param' + self.kwargs['param_name'])
super(DeleteParamView, self).__init__(*args, **kwargs)
Unfortunately self.kwargs cannot be accessed yet; at least I get 'DeleteParamView' object has no attribute 'kwargs'
I also tried to override def get_model() but that does not exist as part of the CBV.

Override the get_queryset method.
def get_queryset(self):
Model = apps.get_model('persons', 'param' + self.kwargs['param_name'])
return Model.objects.all()

Related

Django: one url search in two models (cbv)

Using Django, I'm looking for a way to use one url patern (with slug) to query one model and if nothing is found query a second model. I'm using Class Based Views.
I am following this answer, and the next View is being called. But then I get the following error:
"Generic detail view must be called with either an object pk or a slug."
I can't figure out how to pass the slug to the next View.
My url:
url(r'^(?P<slug>[-\w]+)/$', SingleView.as_view(), name='singleview'),
My CBV's:
class SingleView(DetailView):
def dispatch(self, request, *args, **kwargs):
post_or_page_slug = kwargs.pop('slug')
if Page.objects.filter(slug=post_or_page_slug).count() != 0:
return PageDetailView.as_view()(request, *args, **kwargs)
elif Post.objects.filter(slug=post_or_page_slug).count() != 0:
return PostDetailView.as_view()(request, *args, **kwargs)
else:
raise Http404
class PageDetailView(DetailView):
model = Page
template_name = 'page-detail.html'
class PostDetailView(DetailView):
model = Post
template_name = 'post-detail.html'
The problem is that you are popping the slug, which removes it from kwargs. This means that the slug is not getting passed to the view.
You can change it to:
post_or_page_slug = kwargs.pop['slug']
I would usually discourage calling MyView.as_view(request, *args, **kwargs) inside another view. Class based views are intended to be extended by subclassing, not by calling them inside other views.
For the two views in your example, you could combine them into a single view by overriding get_object and get_template_names.
from django.http import Http404
class PageOrPostDetailView(DetailView):
def get_object(self):
for Model in [Page, Post]:
try:
object = Model.objects.get(slug=self.kwargs['slug'])
return object
except Model.DoesNotExist:
pass
raise Http404
def get_template_names(self):
if isinstance(self.object, Page):
return ['page-detail.html']
else:
return ['post-detail.html']

Passing arguments to ModelForm through FormSet

I'm kinda new to Formsets and I'm stuck at a problem.
I use a Modelform to allow the creation of a new object.
class AddUpdateEntryForm(forms.ModelForm):
class Meta:
model = Zeit
exclude = ('mitarbeiter', 'user_updated')
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(AddUpdateEntryForm, self).__init__(*args, **kwargs)
self.fields['projekt'].queryset = Projekt.objects.filter(firma=Mitarbeiter.objects.get(user_id=self.user).firma_id)
That form gets it's arguments from the view:
form = AddUpdateEntryForm(user=entry_user, initial=initial)
Now, I want to display multiple instances of that form on a single page.
I use:
forms.py:
AddEntryFormSet = formset_factory(form=AddUpdateEntryForm)
and
views.py:
formset = AddEntryFormSet(initial=initial)
which works fine, but only when I comment out the "self.user...." and "self.fields...." lines from ModelForm Class.
I tried several ways of passing the argument from the call inside the view to the ModelForm.
Is there a proper way to do this?
Thanks in advance
Conrad
It should be possible to subclass BaseModelFormset so that the user is passed to each form when it is constructed. However, that's quite tricky.
A simpler technique is to define a function that creates a model form for a given user, and dynamically create the model form class in the view.
def create_form(user):
"""Returns a new model form which uses the correct queryset for user"""
class AddUpdateEntryForm(forms.ModelForm):
class Meta:
model = Zeit
exclude = ('mitarbeiter', 'user_updated')
def __init__(self, *args, **kwargs):
super(AddUpdateEntryForm, self).__init__(*args, **kwargs)
self.fields['projekt'].queryset = Projekt.objects.filter(firma=Mitarbeiter.objects.get(user_id=user).firma_id)
return AddUpdateEntryForm
The closure of user in the function means that you can set the queryset correctly. Note that the __init__ method takes the same arguments as its parent class, so we no longer have any problems when we use modelformset_factory in the view.
AddUpdateEntryForm = create_form(user)
AddEntryFormSet = modelformset_factory(model=Zeit, form=AddUpdateEntryForm)

How to set initial data for Django admin model add instance form?

How can I set an initial value of a field in the automatically generated form for adding a Django model instance, before the form is displayed? I am using Django 1.3.1.
My model is the following:
class Foo(models.Model):
title = models.CharField(max_length=50)
description = models.TextField()
and the current admin form is really nothing special
class FooAdmin(admin.ModelAdmin):
ordering = ('title',)
When I use the admin page to add a new instance of Foo, I get a nice form with empty fields for title and description. What I would like is that the description field is set with a template that I obtain by calling a function.
My current best attempt at getting there is this:
def get_default_content():
return 'this is a template for a Foo description'
class FooAdminForm(django.forms.ModelForm):
class Meta:
model = Foo
def __init__(self, *args, **kwargs):
kwargs['initial'].update({'description': get_default_content()})
super(FooAdminForm, self).__init__(self, *args, **kwargs)
class FooAdmin(admin.ModelAdmin):
ordering = ('title',)
form = FooAdminForm
but if I try this I get this Django error:
AttributeError at /admin/bar/foo/add/
'FooForm' object has no attribute 'get'
Request Method: GET
Request URL: http://localhost:8000/admin/bar/foo/add/
Django Version: 1.3.1
Exception Type: AttributeError
Exception Value: 'FooForm' object has no attribute 'get'
Exception Location: /www/django-site/venv/lib/python2.6/site-packages/django/forms/widgets.py in value_from_datadict, line 178
I don't know what is wrong here, and what I should do to make it work. What I also find strange about this error (apart from the fact that I see it at all) is that there is no FooForm in my code at all?
Alasdair's approach is nice but outdated. Radev's approach looks quite nice and as mentioned in the comment, it strikes me that there is nothing about this in the documentation.
Apart from those, since Django 1.7 there is a function get_changeform_initial_data in ModelAdmin that sets initial form values:
def get_changeform_initial_data(self, request):
return {'name': 'custom_initial_value'}
You need to include self as the first argument in your __init__ method definition, but should not include it when you call the superclass' method.
def __init__(self, *args, **kwargs):
# We can't assume that kwargs['initial'] exists!
if 'initial' not in kwargs:
kwargs['initial'] = {}
kwargs['initial'].update({'description': get_default_content()})
super(FooAdminForm, self).__init__(*args, **kwargs)
Having said that, a model field can take a callable for its default, so you may not have to define a custom admin form at all.
class Foo(models.Model):
title = models.CharField(max_length=50)
description = models.TextField(default=get_default_content)
More then 3 years later,
But actually what you should do is override admin.ModelAdmin formfield_for_dbfield .. like this:
class FooAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(FooAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'description':
field.initial = 'My initial description'
elif db_field.name == 'counter':
field.initial = get_counter() + 1
return field
Cheers;
When adding new objects, it is convenient to use get_changeform_initial_data() as suggested by Wtower.
However, when changing existing objects, that does not work (see source).
In that case, you could extend ModelAdmin.get_form() as follows (using the OP's example):
def get_form(self, request, obj=None, change=False, **kwargs):
if obj and not obj.description:
obj.description = get_default_content()
return super().get_form(request, obj, change, **kwargs)

adding new form fields dynamically in admin

I am trying to add dynamically new form fields (I used this blog post), for a form used in admin interface :
class ServiceRoleAssignmentForm(forms.ModelForm):
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
self.fields['test'] = forms.CharField(label='test')
class ServiceRoleAssignmentAdmin(admin.ModelAdmin):
form = ServiceRoleAssignmentForm
admin.site.register(ServiceRoleAssignment, ServiceRoleAssignmentAdmin)
However, no matter what I try, the field doesn't appear on my admin form ! Could it be a problem related to the way admin works ? Or to ModelForm ?
Thank for any help !
Sébastien
PS : I am using django 1.3
When rendering your form in template, fields enumerating from fieldsets variable, not from fields. Sure you can redefine fieldsets in your AdminForm, but then validations will fail as original form class doesn't have such field. One workaround I can propose is to define this field in form definition statically and then redefine that field in form's init method dynamically. Here is an example:
class ServiceRoleAssignmentForm(forms.ModelForm):
test = forms.Field()
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
# Here we will redefine our test field.
self.fields['test'] = forms.CharField(label='test2')
I actually have a the same issue which I'm working through at the moment.
While not ideal, I have found a temporary workaround that works for my use case. It might be of use to you?
In my case I have a static name for the field, so I just declared it in my ModelForm. as normal, I then override the init() as normal to override some options.
ie:
def statemachine_form(for_model=None):
"""
Factory function to create a special case form
"""
class _StateMachineBaseModelForm(forms.ModelForm):
_sm_action = forms.ChoiceField(choices=[], label="Take Action")
class Meta:
model = for_model
def __init__(self, *args, **kwargs):
super(_StateMachineBaseModelForm, self).__init__(*args, **kwargs)
actions = (('', '-----------'),)
for action in self.instance.sm_state_actions():
actions += ((action, action),)
self.fields['_sm_action'] = forms.ChoiceField(choices=actions,
label="Take Action")
if for_model: return _StateMachineBaseModelForm
class ContentItemAdmin(admin.ModelAdmin):
form = statemachine_form(for_model=ContentItem)
Now as I mentioned before, this is not entirely 'dynamic', but this will do for me for the time being.
I have the exact same problem that, if I add the field dynamically, without declaring it first, then it doesn't actually exist. I think this does in fact have something to do with the way that ModelForm creates the fields.
I'm hoping someone else can give us some more info.
Django - Overriding get_form to customize admin forms based on request
Try to add the field before calling the super.init:
def __init__(self, *args, **kwargs):
self.fields['test'] = forms.CharField(label='test')
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)

Django: Force select related?

I've created a model, and I'm rendering the default/unmodified model form for it. This alone generates 64 SQL queries because it has quite a few foreign keys, and those in turn have more foreign keys.
Is it possible to force it to always (by default) perform a select_related every time one of these models are returned?
You can create a custom manager, and simply override get_queryset for it to apply everywhere. For example:
class MyManager(models.Manager):
def get_queryset(self):
return super(MyManager, self).get_queryset().select_related('foo', 'bar')
(Prior to Django 1.6, it was get_query_set).
Here's also a fun trick:
class DefaultSelectOrPrefetchManager(models.Manager):
def __init__(self, *args, **kwargs):
self._select_related = kwargs.pop('select_related', None)
self._prefetch_related = kwargs.pop('prefetch_related', None)
super(DefaultSelectOrPrefetchManager, self).__init__(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
qs = super(DefaultSelectOrPrefetchManager, self).get_queryset(*args, **kwargs)
if self._select_related:
qs = qs.select_related(*self._select_related)
if self._prefetch_related:
qs = qs.prefetch_related(*self._prefetch_related)
return qs
class Sandwich(models.Model):
bread = models.ForeignKey(Bread)
extras = models.ManyToManyField(Extra)
# ...
objects = DefaultSelectOrPrefetchManager(select_related=('bread',), prefetch_related=('extras',))
Then you can re-use the manager easily between model classes. As an example use case, this would be appropriate if you had a __unicode__ method on the model which rendered a string that included some information from a related model (or anything else that meant a related model was almost always required).
...and if you really want to get wacky, here's a more generalized version. It allows you to call any sequence of methods on the default queryset with any combination of args or kwargs. There might be some errors in the code, but you get the idea.
from django.db import models
class MethodCalls(object):
"""
A mock object which logs chained method calls.
"""
def __init__(self):
self._calls = []
def __getattr__(self, name):
c = Call(self, name)
self._calls.append(c)
return c
def __iter__(self):
for c in self._calls:
yield tuple(c)
class Call(object):
"""
Used by `MethodCalls` objects internally to represent chained method calls.
"""
def __init__(self, calls_obj, method_name):
self._calls = calls_obj
self.method_name = method_name
def __call__(self, *method_args, **method_kwargs):
self.method_args = method_args
self.method_kwargs = method_kwargs
return self._calls
def __iter__(self):
yield self.method_name
yield self.method_args
yield self.method_kwargs
class DefaultQuerysetMethodCallsManager(models.Manager):
"""
A model manager class which allows specification of a sequence of
method calls to be applied by default to base querysets.
`DefaultQuerysetMethodCallsManager` instances expose a property
`default_queryset_method_calls` to which chained method calls can be
applied to indicate which methods should be called on base querysets.
"""
def __init__(self, *args, **kwargs):
self.default_queryset_method_calls = MethodCalls()
super(DefaultQuerysetMethodCallsManager, self).__init__(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
qs = super(DefaultQuerysetMethodCallsManager, self).get_queryset(*args, **kwargs)
for method_name, method_args, method_kwargs in self.default_queryset_method_calls:
qs = getattr(qs, method_name)(*method_args, **method_kwargs)
return qs
class Sandwich(models.Model):
bread = models.ForeignKey(Bread)
extras = models.ManyToManyField(Extra)
# Other field definitions...
objects = DefaultQuerysetMethodCallsManager()
objects.default_queryset_method_calls.filter(
bread__type='wheat',
).select_related(
'bread',
).prefetch_related(
'extras',
)
The python-mock-inspired MethodCalls object is an attempt at making the API more natural. Some might find that a bit confusing. If so, you could sub out that code for an __init__ arg or kwarg that just accepts a tuple of method call information.
Create a custom models.Manager and override all the methods (filter, get etc.) and append select_related onto every query. Then set this manager as the objects attribute on the model.
I would recommend just going through your code and adding the select_related where needed, because doing select_related on everything is going to cause some serious performance issues down the line (and it wouldn't be entirely clear where it's coming from).