I've spent quite a bit of time setting up the ModelAdmin for my models, and I want to be able to basically reuse it elsewhere on my site, i.e., not just in the admin pages.
Specifically, I want to further subclass the ModelAdmin subclass I created and override some of the methods and attributes to be non-admin specific, then use that second subclass to create a form from a model in one of my ordinary (non-admin) views. But I'm not sure how to get the form out of the ModelAdmin subclass or how to bind it to a specific model.
So for instance, my admin.py looks something like this:
from django.contrib import admin
class MyAdmin(admin.ModelAdmin):
fields = ['my_custom_field']
readonly_fields = ['my_custom_field']
def my_custom_field(self, instance):
return "This is my admin page."
And this works real nice. Now in I want to subclass this as:
class MyNonAdmin(MyAdmin):
def my_custom_field(self, instance):
return "This is NOT my admin page!"
Now I want to use the MyNonAdmin in a view function to generate a form bound to a particular model, and use that form in a template. I'm just not sure how to do that.
Update
I found how to render the form, but it doesn't include any of the readonly_fields. Rendering the form looks like this:
def view_func(request, id):j
res = get_object_or_404(ModelClass, pk=int(id))
admin = MyNonAdmin(ModelClass, None)
form = admin.get_form(request, res)
return render(request, 'my_template.html', dict(
form=form,
))
But as I said, it only renders the form itself, not the other readonly_fields which is what I really care about.
Related
I have 10 Django Class Based Views and I want to display them read-only to the user.
I want the whole form to be read-only, not only some values. Submitting the form should be disabled on the client (HTML) and a second time on the server (POST not allowed).
Is there a MixIn or an other simple solution?
Here's a mixin that does two simple things:
Sets html attributes for all fields in form for disabled andreadonly.
Overrides the form_valid method of your CBV so that no model saving ever happens; instead, the template is rendered (just as if there was no submitted data). The user, this way, does not cause any action if they submitted the form.
Form field errors may appear next to disabled fields if you are rendering the full form in your template; solve this by either erasing the form's error dictionary or by rendering each field individually without errors.
from django.views.generic.edit import FormMixin, ModelFormMixin
class ReadOnlyModelFormMixin(ModelFormMixin):
def get_form(self, form_class=None):
form = super(ReadOnlyModelFormMixin, self).get_form()
for field in form.fields:
# Set html attributes as needed for all fields
form.fields[field].widget.attrs['readonly'] = 'readonly'
form.fields[field].widget.attrs['disabled'] = 'disabled'
return form
def form_valid(self, form):
"""
Called when form is submitted and form.is_valid()
"""
return self.form_invalid(form)
Extending this concept for a non-model FormView is pretty simple; inherit from class FormMixin instead. :)
To disallow POST requests in general for class-based views you could use the following mixin:
class DisallowPostMixin(object):
def post(self, request, *args, **kwargs):
return self.http_method_not_allowed(self, request, *args, **kwargs)
If you also want to disable certain form fields etc. you could add the get_form method from Ian Price's answer.
You can hack it through middleware. On request - check view name and request method (if post - redirect), on response - add input attrs in response.content. But mixin - best solution.
I am trying to design a Django application that facilitates the lending and borrowing of musical instruments between musicians.
I have one template page that includes the form to post an instrument for lending or borrowing, and another template that includes the form for searching for available listings.
The difference between the two views other than the templates that are rendered (slightly different designs and buttons) is the name of the form they add to the context (i.e. PostForm() and SearchForm())
Basically, I have two views with almost completely the same code. This is bad practice usually.
Is there any way I can consolidate the two views into a "super-view" of sorts so that changes to one view are automatically made across both? I want to avoid duplicate code wherever possible.
This is very easy to do with Class Based Views (CBV).
For example, you may use django.views.generic.FormView, as follows in your views.py:
from django.views import generic
class ClassyFormView(generics.FormView): # Of course name the view whatever you like
# Note that I'm not setting any of the specific attributes here
# As I am planning on overriding this view for each form's specifics
# This is where you may override methods of the FormView
def get_context_data(self, *args, **kwargs):
""" This method allows you to edit the context passed to the template """
context = super(ClassyFormView, self).get_context_data(*args, **kwargs) # Get context from FormView
# Add variables to the context data
context['message'] = "Hello World!"
return context
def form_valid(self, form):
"""
This method is called once the form has been instantiated with
POST data and has been validated
"""
# Do whatever you want after the form is validated
print(form.cleaned_data['some_field'])
def form_invalid(self, form):
# Do something if the form is invalid
pass
You can then override your custom class, to maintain the specific things it does over FormView, but use the correct form class:
class SearchFormView(ClassyFormView):
form_class = SearchForm
class PostFormView(ClassyFormView):
form_class = PostForm
Note that you can (and probably will) also set fields such as prefix, success_url, and template_name, as well as override tons of other methods that may be useful!
Note that if your forms are ModelForms, then you will probably want to use one of the model specific generic form views, such as CreateView or UpdateView. Using these will allow you to access the object that the form is acting on. So, after setting the correct model in the class, your form_valid method may look like this:
def form_valid(self, form):
self.object = form.save(commit=False)
# Do stuff here to the object before you save it
# Such as altering or setting fields
self.object.some_field = "I'm a string"
self.object.save()
I can't explain everything about CBV, or even the class based form views here, so make sure to look at further documentation:
Django Class-Based-View Inspector is a really awesome site that not many people seem to know about! Usually easier than diving into the source code.
Relevant Django docs:
CBVs
Generic editing views
Decided to add a few more details that may be helpful.
The generic views specify defaults for their class attributes, but unless you're being really generic, it's usually a good idea to override them. Some explanation on the specific attributes:
prefix specifies a prefix for the form. Useful in cases where using multiple forms.
Defaults to None.
If you require more advanced logic, you can set the prefix by returning it in the get_prefix() method.
success_url specifies where the form will redirect to on success. For the model form views, this will default to the model's get_absolute_url()
Can be set by returning success url in get_success_url() method
template_name specifies the name of the template that the view will display upon a get request
Can be set by returning template name in get_template_name() method.
Configuring URLs for CBV is easy, too. Use the as_view() method as follows:
url(r'^some_url/', SearchFormView.as_view(), name="some-url")
I am overriding the save method on a Django model. There are some cases where I do not save the model. In these cases, I can't seem to be able to figure out how to conditionally override the "The citation 1111 was added successfully." message that is shown after returning back to the admin list interface (as opposed to the entry form interface).
I don't think you can override that just by overriding save Django's admin interface uses model forms and the messages framework.
I think something like this happens, it's more complicated than this but more or less:
models.py
class MyModel(models.Model):
foo = models.CharField(...)
bar = models.CharField(...)
def save(self, *args, **kwargs):
if self.foo == self.bar: # We only save if foo == bar
super(MyModel, self).save(*args, **kwargs)
forms.py (Django admin uses model forms, so this is an example)
class MyModelForm(ModelForm):
class Meta:
model = MyModel
views.py
def save(request):
if request.method == 'POST':
form = MyModelForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'MyModel was saved!.')
Now regardless of what form.save() actually did the message is still sent out anyway, we have no way of knowing if you saved or not in your overridden save method and this is probably whats happening in the django admin system.
An alternative would be to create a custom model form for the admin to use for this model and define a custom validation method, so the form doesn't validates unless foo == bar. Or you could override the save method on the form, you'll need to have a look around the django admin code, it probably is possible, just a bit trixy :p
I get an error in my class AuthorCreateForm when I submit my form.
NameError
self is not defined
How do I use a CreateForm?
I have created a class in my Author.py file
from django.views.generic import TemplateView, ListView, CreateView
from books.models import Author, Publisher, Book
from books.forms import AuthorForm
class AuthorCreateView(CreateView):
objAuthorForm = AuthorForm(self.request.POST)
if(objAuthorForm.save()):
success = "Form saved!"
else:
error = "There was an error!"
and I have a html template which submits to /Author/Create
and I have the following line in my urls.py
('^authors/create/$', Author.AuthorCreateView.as_view()),
I render the form at this URL
('^authors/new/$', TemplateView.as_view(template_name="author_new.html")),
I find the class based views confusing, does anyone have a good tutorial on how to use it for CRUD operations?
Thanks
What you have is a python error -- self is not defined. self is generally what refers to the class instance itself on class methods.
Anyways, I agree, it's brand spanking new and not as documented. I think looking at the source is absolutely key at this point.
To get comfortable with class based views, I'd start by subclassing django.views.generic.base.View, which implements only a few methods, namely attempting to call a function on the class based on the request method (post, get, head, - look at source).
For example, here's the first step to replace view functions with the new view classes:
class MyClassBasedView(View):
def get(self, request):
# behave exactly like old style views
# except this is called only on get request
return http.HttpResponse("Get")
def post(self, request):
return http.HttpResponse("Post")
(r'^foobar/$', MyClassBasedView.as_view())
Back to your specific question:
All TemplateView.as_view() does is render the template - CreateView is a combination of several other classes that handle ModelForms and template rendering (TemplateView).
So, for a very basic example, look to the docs for what class mixins are used by CreateView.
We see it implements TemplateResponseMixin, ModelFormMixin, and ProcessFormView, each containing a list of methods for those classes.
The most basic CreateView
At the most basic level, provide CreateView's ModelFormMixin with the model or custom ModelForm class as documented here.
Your CreateView class would look something like the following
class AuthorCreateView(CreateView):
form_class = AuthorForm
template_name = 'author_new.html'
success_url = 'success'
With those 3 core attributes set, call it in your URLs.
('^authors/create/$', Author.AuthorCreateView.as_view()),
Render the page and you'll see your ModelForm passed to the template as form, handling the form validation step (passing in request.POST / re-render if invalid), as well as calling form.save() and redirecting to the success_url.
Start overriding the class methods
To customize behavior, start overriding the methods documented for the mixins.
Remember that you simply need to return an HttpResponse from one of these methods just like any regular view function.
Example overriding form_invalid documented in ModelFormMixin:
class AuthorCreateView(CreateView):
form_class = AuthorForm
template_name = 'author_new.html'
success_url = 'success'
def form_invalid(self, form):
return http.HttpResponse("form is invalid.. this is just an HttpResponse object")
This per-method overriding starts becoming extremely useful as your forms grow more advanced and ultimately lets you build huge forms with a handful of lines of code, overriding only what is necessary.
Say you want to pass your form custom parameters such as the request object (very common if you need access to the user in the form): you merely need to override get_form_kwargs.
class MyFormView(FormView):
def get_form_kwargs(self):
# pass "user" keyword argument with the current user to your form
kwargs = super(MyFormView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
Class based views are a shining example of smart class usage. It gave me a great intro towards building my own mixins for views and python classes in general. It is saving countless hours.
Wow this got long. To think it started as a mere URL to the docs comment :)
First a little background:
I have an Event model that has various event_types. I want to break one of those event types, 'Film', into it's own admin. I have the basic functionality in place: a proxy model inheriting from Event, named Film, a custom manager for that proxy model that filters it to only 'film' event types, and it's own ModelAdmin.
The problem is with the reverse. I now need to filter out films from the main Event admin. I don't want to alter the Event model or its default manager, because the impact would be too widespread. So, I tried creating another proxy model, EventAdminProxy, with the sole purpose of providing a filtered list of events in the admin. I then register this model, instead of Event, with the existing ModelAdmin.
This obviously works, but it has the unfortunate side-effect of altering the URLs in the admin. Instead of the changelist being at "/admin/event/event/", it's now at "/admin/event/eventadminproxy/".
What I'm trying to do is keep this setup, but also keep the old URL. I've tried overloading the ModelAdmin's get_urls method, but from what I can tell, you can't control the full URL there, only what comes after the "/app_label/model_class/" part.
I thought about overriding it in the main urls.py, but can't figure out an acceptable view to tie into. The actual views are only available on the instantiated ModelAdmin object, not the class itself.
Any ideas of how override the URL being used in the admin?
Looking at the Django source, the admin URLs are built in two places, in the ModelAdmin instances, and in the AdminSite instances.
The part you want to change is built in the AdminSite instance (django.contrib.admin.sites.AdminSite), you can subclass that and override the get_urls method. If you look at the second half of the method you'll see this:
# Add in each model's views.
for model, model_admin in self._registry.iteritems():
urlpatterns += patterns('',
url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name),
include(model_admin.urls))
)
There it is adding the model's ._meta.module_name which is just the model's name lowercased (django.db.models.options.Options.contribute_to_class).
An easy way out is to override the Site's get_urls method and add a dict or special case for the Proxy model so it uses a different url instead of model._meta.module_name, something along the lines:
class MyAdminSite(AdminSite):
module_name_dict = {
EventAdminProxy: 'myfunkymodulename'
}
def get_urls(self):
base_patterns = super(MyAdminSite, self).get_urls()
my_patterns = patterns('',)
for model, model_admin in self._registry.iteritems():
if model in self.module_name_dict:
module_name = self.module_name_dict[model]
my_patterns += patterns('',
url(r'^%s/%s/' % (model._meta.app_label, module_name),
include(model_admin.urls))
)
return my_patterns + base_patterns
You could override the queryset-method of your EventModelAdmin and filter the queryset so that Film-Events get excluded.
Something similar to this:
class EventAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(EventAdmin, self).queryset(request)
return qs.exclude(event_type='film')
You could also subclass ChangeList and override the url_for_result() method to customise change urls, (learned from another answer), e.g.:
from django.contrib.admin.views.main import ChangeList
class FooChangeList(ChangeList):
def url_for_result(self, obj):
return '/foos/foo/{obj.pk}/'
class FooAdmin(admin.ModelAdmin):
def get_changelist(self, request, **kwargs):
return FooChangeList
Adapted example for the question:
from django.contrib.admin.views.main import ChangeList
from django.urls import reverse
class FilmAdmin(admin.ModelAdmin):
def get_changelist(self, request, **kwargs):
class FilmChangeList(ChangeList):
def url_for_result(self, obj):
return reverse('admin:events_event_change', args=(obj.pk, ))
return FilmChangeList