I'd like to use the django admin to produce a read-only view of an object which contains an "Edit" button which switches you to the usual change view of the same object.
I know how to use the readonly attributes to produces a read-only view, but I don't know how to produce two views, one read-only and one that allows changes.
I'd like to reuse as much of the admin interface for this as possible, rather than writing a view from scratch.
Note that this question isn't about permissions: all users will have permission to change the objects. It's just that I would prefer that they not use the change_view unless they do intend to make changes, reducing the risk of accidental changes or simultaneous changes.
Here's an answer that literally does what I asked with only a few lines of code and just a couple of template changes:
class MyModelAdmin(admin.ModelAdmin):
fieldsets = [...]
def get_readonly_fields(self, request, obj=None):
if 'edit' not in request.GET:
return <list all fields here>
else:
return self.readonly_fields
Now the usual URL for the change_form will produce a read only change_form, but if you append "?edit=1" to the URL, you will be able to edit.
The change_form template can also be customized depending on whether "?edit=1" is in the URL. To do this, put 'django.core.context_processors.request' in TEMPLATE_CONTEXT_PROCESSORS in settings.py, and then use request.GET.edit in the template.
For example, to add an "Edit" button when not in edit mode, insert
{% if not request.GET.edit %}
<li>Edit</li>
{% endif %}
just after <ul class="object-tools"> in change_form.html.
As another example, changing change_form.html to contain
{% if save_on_top and request.GET.edit %}{% submit_row %}{% endif %}
will mean that the submit row will only be shown in edit mode. One can also hide the Delete buttons on inlines, etc, using this method.
For reference, here is what I put in settings.py:
TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.contrib.messages.context_processors.messages',
# Above here are the defaults.
'django.core.context_processors.request',
)
I'd suggest to reconsider using custom views. With the help of generic DetailView, you'll need to write literally two lines of code. The template won't require much work either. You just extend standard change_form.html template, overriding field_sets block.
I know how to use the readonly attributes to produces a read-only view, but I don't know how to produce two views, one read-only and one that allows changes.
You actually can register one model in the admin twice[1], using proxy models. (There're some inconsistencies with permissions for proxy models, but it may not be a problem in your case.)
It seems to be possible to register multiple admin sites[2], too.
I'd like to reuse as much of the admin interface for this as possible, rather than writing a view from scratch.
Interface reuse as such has little to do with views, being mostly template- and style-related thing. View, however, should provide the template context necessary for interface reuse, as you correctly pointed out.
If you decide to go with multiple views per one ModelAdmin, then it might be useful for you to check how django-reversion project implements its admin integration: reversion/admin.py.
References
[1] Multiple ModelAdmins/views for same model in Django admin
[2] Registering Multiple Admin Sites
You will need to change template django admin uses for model form. Make it readonly and add a button to original template linked to another url.
Note:
I highly discourage this approach, you will definitely not prevent simultaneous changes. This should be solved with locking.
Also, I recommend using django-reversion for keeping history of objects and eliminating "accidental changes" risk.
You could create a custom view and display your object there.
To create a custom view in an admin module, override the get_urls() method :
class MyAdmin(admin.ModelAdmin):
…
def get_urls(self):
urls = super(MyAdmin, self).get_urls()
my_urls = patterns('',
url(r'^custom_view/(?P<my_id>\d+)/$', self.admin_site.admin_view(self.custom_viem), name='custom_view')
)
return my_urls + urls
def custom_view(self, request, my_id):
"""Define your view function as usual in views.py
Link to this view using reverse('admin:custom_view')
"""
from myapp import views
return views.custom_view(request, my_id, self)
In views.py :
def custom_view(request, object_id, model_admin):
admin_site = model_admin.admin_site
opts = model_admin.model._meta
my_object = get_object_or_404(MyObject, pk=object_id)
# do stuff
context = {
'admin_site': admin_site.name,
'opts': opts,
'title': _('My custom view'),
'root_path': '%s' % admin_site.root_path,
'app_label': opts.app_label,
'my_object': my_object,
}
return render_to_response('my_template.html', context,
context_instance=RequestContext(request))
In your template, use {% extends "admin/base_site.html" %} to keep the admin look and feel.
The below code is implementation of read-only admin using proxy models.
Models.py
//real model
class CompetitionEntry(models.Model):
pass
//Proxy model
class ReviewEntry(CompetitionEntry):
class Meta:
proxy = True
def save(self, *args, **kwargs):
pass
admin.py
//editable admin
class CompetitionEntryAdmin(admin.ModelAdmin):
pass
admin.site.register(CompetitionEntry, CompetitionEntryAdmin)
// read-only admin (assign only "change" permission for this)
class ReviewEntryAdmin(admin.ModelAdmin):
pass
admin.site.register(ReviewEntry, ReviewEntryAdmin)
Related
Given the following situation:
# models.py
class Book(Model):
pass
# views.py
class BookDetail(DetailView):
model = Book
# books/urls.py
urlpatterns += [path('detail/<int:pk>', BookDetail.as_view(), 'book_detail')]
# page/urls.py
urlpatterns += [path('books/', include('books.urls'))]
I can load the detail view for the object with the private key id 42 at /books/detail/42/. If I am in another request with a completely different path and hold a reference to the object with the private key id 42, is there an "official" or builtin way to generate the url /books/detail/42/? Preferably outside of templating, so I can respond with a JSON.
Or is the idiomatic way to parametrize the path elements (books and detail) and just rebuild it myself?
Yes, you can make use of get_absolute_url for model-specific views and the {% url ... %} template tag to calculate the name of a view.
Model-specific views
If a Model has a specific view to show details, you can implement a get_absolute_url [Django-doc] on your model, like:
from django.urls import reverse
class Book(Model):
def get_absolute_url(self):
return reverse('book_detail', kwargs={'pk': self.pk})
Here we use reverse [Django-doc] to "calculate" the URL for the given name of the view (specified in your path(..., name='book_detail')) with as pk parameter, the pk of self.
In your template you can then write:
{{ my_object }}
with my_object the name of the variable in your template.
It is note that the redirect [Django-doc] function understands the get_absolute_url, and thus you can write return redirect(my_object) in a view, and it will automatically call get_absolute_url to redirect to the proper view.
If you serialize a model with the Django REST framework, then you can reuse the get_absolute_url as well, by using a URLField [drf-doc] for example:
from rest_framework.serializers import ModelSerializer, URLField
class BookSerializer(serializers.ModelSerializer):
absolute_url = serializers.URLField(
read_only=True,
source='get_absolute_url'
)
This specific use case is documented in the documentation of the Django REST framework.
Making use of {% url ...%} in the template
You can also calculate the URL of a view, by using the {% url ... %} template tag [Django-doc]. You can for example write:
{{ my_object }}
to calculate the URL, just like we did with the get_absolute_url. It is however useful as well for other, non model-specific views.
You should use Django Rest Framework
All built-in, you just have to set up.
I have a class-based view FooCreate and I want to use it on two different pages:
normal create view: With all the normal header and footer parts: Extending my base.html
in popup: Here I want no visible header and footer part: Extending (the not existing yet) base_popup.html
I would like to implement this without a single "if", since I like condition-less code :-)
I would advise you to checkout how Django Admin handles this with IS_POPUP_VAR.
Basically, Django Admin uses a '_popup' parameter passed in the querystring. You can then pass a "base_layout" variable to context.
class MyView(View):
def get_context_data(self, **kwargs):
if '_popup' in request.GET:
kwargs['base_layout'] = 'base_popup.html'
return super().get_context_data(**kwargs)
And your templates would start with:
{% extends base_layout|default:"base.html" %}
Let's say that, as part of an administrative process, thousands of values are created offline. These values are entered into a dead-simple model:
class Foo(models.Model):
value = models.CharField(max_length=32)
I'd like to have a field on the model creation page that allows the user to enter (copy-paste) 1000 values in, and as a result, 1000 rows would be created in the table.
If I can add a text field to the model creation, all I have to do is parse out the values and call Foo.create for each. How would I add this free-form field, and how would I go about processing it when the user hits the Save button? ...or is there entirely a different way that I should be going about this?
I realize my comment is more of an answer now.
Sure, why not? You hardly even need django for this. You could just create a <textarea name="foo"></textarea>, and in your view parse the data by line break.
Create a custom admin view via Admin.get_urls and write a custom view for your bulk create page.
https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_urls
Here's a copy and paste out of the live example for get_urls
class MyModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(MyModelAdmin, self).get_urls()
my_urls = [
url(r'^my_view/$', self.my_view),
]
return my_urls + urls
def my_view(self, request):
# ...
context = dict(
# Include common variables for rendering the admin template.
self.admin_site.each_context(request),
# Anything else you want in the context...
)
if request.method == 'POST':
for line in request.POST['bulk-create-paste'].split('\n'):
Foo.objects.create(myfield=line)
return TemplateResponse(request, "sometemplate.html", context)
sometemplate.html
<form method="POST">
<p>Paste bulk create info.</p>
<textarea name="bulk-create-paste">
</textarea>
</form>
{% include 'django.contrib.auth.views.login' %}
I don't want to write everything by hand.. I hate this really, django full of automatic stuff.
Goal is to include registration/login.html into base.html, so that I could have this form in every page
If I include only template itself (registration/login.html), problem appears that "form.login", I mean "form" var is not defined because this one comes from VIEW which called when you going to login url. So how can I call that view MANUALLY with include or at least to grab django.contrib.auth.views.login variables by my self in my own view and pass then to base.html?
P.s. It's not just about login form, I think there will be more situations like this
I have found better solution in #django irc.
They called inclusion tags
I'll give you my code, because I got lot's of problem learning new stuff in django =)
file: templatetags/form_login.py
from django import template
register = template.Library()
from django.contrib.auth.forms import AuthenticationForm
#register.inclusion_tag('registration/login.html')
def form_login():
return { 'form': AuthenticationForm() }
Now you can have your form anywhere, this will prerender template and THAT'S IT! no stupid context processors which requires to modify whole project settings.py, which is really sux if you writing stand alone little application..
If you need login-form on every page
Create a context processor:
def login_form_processor(request):
return {
'login_form': LoginForm(request.POST or None)
}
Add it to settings.CONTEXT_PROCESSORS.
Include the template for login form:
{% with login_form as form %}
{% include "registration/login.html" %}
{% endwith %}
You can also make you form lazy-loading, so form will not be created until it is used for the first time.
from django.utils improt functional
def login_form_processor(request):
create_login_form = lambda: LoginForm(request.POST or None)
return {
'login_form': functional.lazy(create_login_form, LoginForm)
}
But I guess you won't want the lazy-loading feature, because login-form is cheap to initialize.
Reusing views
Concerning the "grabbing variables" part from your question: you cannot grab variable from view. Django view is method which returns response object. You can not get variables from response. However some of views accept extra_context and other attributes. Those attributes allow you to configure those views in urls, or to wrap them with your own view, for example:
def my_login_view(request):
some_extra_data = get_some_data()
extra_context = {
'some_extra_var': some_extra_data
}
return login_view(request, extra_context=extra_context, template="my_template.html")
This is not exactly grabbing the variables from views, more like augmentation of existing views.
If you expect to have more situations like this, do less data-porcessing in views. Call some methods which checks for permissions. Collect some data from context-processors. Return rendered response. Now you can reuse the data in other views.
You can specify the action on the form html to point to the URL that accesses the corresponding view.
If you want a form, say called as login_form always populated in all templates, then put it in the context_processors.
Browsing the code for django.contrib.auth.views, you will see that the variables form, site and *site_name* are passed to the template.
Either you (1) provide your custom registration form or (2) you can just import django.contrib.auth.forms.AuthenticationForm in your view if you want to use it.
I have a page, index.html, that contains both a login and registration form. I have a couple of questions about getting this to work properly
My URLConfig looks like this:
urlpatterns = patterns('djangoproject1.authentication.views',
(r'^$',direct_to_template,{'template':'authentication/index.html'}),
(r'^register/$','register'),
)
1) Using the Django book is a guide, my form looks like this:
<h1>Register</h1>
<form action="/register/" method="post">
{{ form.as_p }}
<input type="submit" value="Register">
</form>
Of course, since the file is index.html, the form doesn't appear when I just go to the page. Do I need a "view" to handle visiting index.html rather than a direct_to_template?
2) My Register code looks like this:
def register(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
new_user = form.save()
return HttpResponseRedirect("/register/success/")
else:
form = UserCreationForm()
return render_to_response("authentication/index.html", {'form': form})
This is the django authentication built-in stuff. Do people actually use it? It seems limited. I know I can add more fields to the Django User by using a user profile or something, but what about the UserCreationForm? Should I roll my own form? Should it inherit from UserCreationForm somehow?
direct_to_template by itself can neither produce nor handle forms -- it simply renders a request directly to a template, as its name describes.
You might look into django-registration for registration.
If you're putting two forms on the same page, you'll need a custom view that is capable of rendering and handling both forms, though multi-form pages are notoriously tricky to work with properly. If you have separate forms (and submit buttons), you can add a unique name to each submit input and determine which form (class) to validate and handle based on if name in request.POST.
edit:
After looking more closely at your code, I see that your registration form redirects to a different view; that simplifies things, but you'll still need a custom view for your home page that passes both login and registration forms to the template for rendering.
Alternatively, if you're simply redirecting to pages that handle each form, you can add those forms using direct_to_template's extra_context parameter directly in your urls.py:
from wherever import LoginForm, RegistrationForm
urlpatterns = patterns('djangoproject1.authentication.views',
(r'^$',
direct_to_template,
{
'template': 'authentication/index.html',
'extra_context': {
'reg_form': RegistrationForm(),
'login_form': LoginForm()
}
}
),
(r'^register/$', 'register'),
)
This approach isn't the cleanest, but it's an option if you really wanted to use generic views.
It sounds like you'll probably want to use a different generic view instead of direct_to_tepmlate. Take a look at the create object generic view. I usually just create a view, typically I end up needing to do more than what a generic view will allow me to do easily.