Django Class Based Views with popup functionality - django

I wanted to ask if the following implementation is rational against Django rules of Views and Class Based views.
The scenario, I have implemented a mini administration section for users, not wanting to grant access to the general admin section of Django.
I am ok with the implementation and everything is working properly, recently I added also a small mixin to allow users to add related items the django admin way, the mixin is working properly (based on the original mixin of the Django admin interface). Just wanted to ask if this is a good approach or should I move to another implementation? (this is still a bit rough, made just for tests)
The mixin, we are actually checking against a query args, _popup, this is nearly the same way django admin checks for passed popups, this allows the form to load normally if working on it directly or as a popup when called through the parent form. partials/popup_reponse.html
class PopupMixin(object):
is_popup = False
popup_var = '_popup'
def get_context_data(self, **kwargs):
''' Add the _popup to the context, so we can propagade the templates'''
context = super(PopupMixin, self).get_context_data(**kwargs)
if self.popup_var in self.request.GET:
self.is_popup = True
context['is_popup'] = self.is_popup
return context
def form_valid(self, form):
if self.popup_var in self.request.POST:
self.is_popup = True
''' If this is a popup request, then we are working with a form
this means we save the form, and then override the default form_valid implementation
this allows us to inject newly created items in the form '''
if self.is_popup:
self.object = form.save()
return SimpleTemplateResponse('partials/popup_response.html', {
'value': self.object.id,
'obj': self.object
})
return super(PopupMixin, self).form_valid(form)
Then our create:
class TestCreateView(PopupMixin,CreateView):
form_class = TestForm
template_name = "add.html"
success_url = reverse_lazy('manager-list-tests')
This way in the view, I can make sure that first the base template does load only a bare form and not any other template parts, additionally I get to add a hidden field in the form:
{% if is_popup %}
<input type="hidden" name="_popup" value=1>
{% endif %}
Through this I override the default form_valid to respond with the same template as django admin uses:
<!DOCTYPE html>
<html>
<head><title></title></head>
<body>
<script type="text/javascript">
opener.dismissAddAnotherPopup(window, "{{ value }}", "{{ obj }}");
</script>
</body>
</html>

Related

How to reuse Django's admin foreign key widget in admin intermediate pages

I haven't been able to find the answer anywhere on Django's documentation. Though, I'm not surprised given the question is a bit too complex to ask to a search engine.
I'm in a situation where I need to be able reassign a ForeignKey field for one or more entries of a model on Django's admin site.
So far, what I tried to do so using a custom action so that I can select the records I'm interested in and modify them all at once. But, then, I need to select the new related object I want their fk to be reassigned to. So, what I thought to do is an intermediate page where I'd display the fk widget I see all around the admin pages:
But it turns out this widget is really not designed to be publicly used. It's not documented and it's heavily complex to use. So far, I lost several hours digging into Django's code trying to figure how to use it.
I feel like I'm trying to do something really really exotic here so, if there's another solution, I'm all hears.
As shahbaz ahmad suggested, you can use ModelAdmin's autocomplete_fields which creates an select with autocompletion.
But if you're stuck with Django's foreign key widget, because, for instance, you have records which look the same and are indistinguishable in autocomplete, there is a solution.
It turns out ModelAdmin has a get_form method that you can use to retrieve the ModelForm used on the admin page. This method accepts a fields kwargs that you can use to select the fields you want to retrieve in the form. Use it like this:
class MyAdmin(ModelAdmin):
# define the admin subpath to your intermediate page
def get_urls(self):
return [
path(
"intermediate_page/",
self.admin_site.admin_view(self.intermediate_page),
name="intermediate_page",
),
*super().get_urls(),
]
def intermediate_page(self, request):
context = {
# The rest of the context from admin
**self.admin_site.each_context(request),
# Retrieve the admin form
"form": self.get_form(
request,
fields=[], # the fields you're interested in
)
}
return render(request, "admin/intermediate_page.html", context)
If your field is read only – which was my case – there's a workaround to an editable field: you can override the get_readonly_fields method which is called by get_form.
This method accepts an obj parameter which usually takes the model of the object being edited or None when creating a new entry. You can hijack this parameter to force get_readonly_fields exclude fields from read only fields:
def get_readonly_fields(self, request, obj=None):
readony_fields = super().get_readonly_fields(request, obj)
if not (
isinstance(obj, dict)
and isinstance(obj.get("exclude_from_readonly_fields"), Collection)
):
return readony_fields
return set(readony_fields) - set(obj["exclude_from_readonly_fields"])
get_form also has this obj parameter which it passes down to get_readonly_fields so you can call it like this:
# the fields you're interested in
include_fields = []
self.get_form(
request,
obj={"exclude_from_readonly_fields": include_fields},
fields=include_fields
)
Override changelist template of YourModelAdmin class to add one more button apart from add button.
#admin.register(YourModel)
class YourModelAdmin(admin.ModelAdmin):
change_list_template = "custom_your_model_change_list.html"
In custom_your_model_change_list.html,
{% extends "admin/change_list.html" %}
{% block object-tools-items %}
<li>
<a class="button" href="{% url 'your_reassign_url_name' %}">Reassign</a>
</li>
{{ block.super }}
{% endblock %}
Mapped a view to 'your_reassign_url_name' to processed your request.
In urls.py,
urlpatterns = [
path('reassign/', YourReassignView, name='your_reassign_url_name'),
]
forms.py,
class ReassignForm(forms.Form):
# your reassign field
reassign_field = forms.ModelChoiceField(queryset='your queryset')
# Select multiple objects
updatable_objects = forms.ModelMultipleChoiceField(queryset='All objects queryset',
widget=forms.CheckboxSelectMultiple)
In views.py, On GET request you render a form with your required fields and on submit your update your data (reassign values) and after that you redirect admin change_list page.
def YourReassignView(request):
if request.method == 'POST':
form = ReassignForm(request.POST)
if form.is_valid():
# you get value after form submission
reassign_field = form.cleaned_data.get('reassign_field')
updatable_objects = form.cleaned_data.get('updatable_objects')
# your update query
YourModel.objects.filter(
id__in=updatable_objects.values('id')
).update(field_name=reassign_field)
#after that you redirect admin change_list page with a success message
messages.success(request, 'Successfully reassign')
return redirect(reverse('admin:app_label_model_name_changelist'))
else:
context_data = {'form': form}
return render(request, 'reassign_template.html', context=context_data)
else:
form = ReassignForm()
context_data = {'form': form}
return render(request, 'reassign_template.html', context=context_data)
In reassign_template.html,
<form method="POST" action="{% url 'your_reassign_url_name' %}">
{% csrf_token %}
{{ form.as_p }}
<br>
<input type="submit" value="submit">
</form>

Django templates not updating after database changes via admin

I am using Django v2.2 admin to change the information on my database but after I change it and refresh the page, the new data is not there, only the old data.
A fix for this if I restart the server, the templates can now fetch the new data that I input.
views.py
# template with context
class Home(TemplateView):
template = 'home.html'
context = { 'bar': Baby.objects.all() }
def get(self, request):
return render(request, self.template, self.context)
home.html
{% for foo in bar %}
{{ foo.name }}
{{ foo.cost }}
{% endfor %}
How I can get the new data by refreshing the page and not restarting the server?
As others mentioned, use get_context_data() method is good idea, because ContextMixin is parent class (not base class, but part of TemplateView's __mro__ Method Resolution Order) of TemplateView which is responsible to pass data from view to template. But, if you want to render template manually using get() method, You should hit on database on every GET request (in your case).
class Home(TemplateView):
template = 'home.html'
def get(self, request):
self.context = {'bar': Baby.objects.all()}
return render(request, self.template, self.context)
Your code does not work, because static variables are initialized only once. In your case context was static variable.
Hope, it helps you.
Can you please try this?
class Home(TemplateView):
template_name = 'home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['bar'] = Baby.objects.all()
return context

Revert objects on site instead of admin using django simple history

I have employed django simple history package on the admin site to be able to track and revert to previous versions of the object of the model. I am designing a web form that allows users to change instances of the model object using model form on django and would like to allow the users to view and revert to previous versions. Also to allow them to see what are the changes compared to the current version.
With the code below I am able to get the list of historical records on my template under histoire.
class CompanyDetailView(LoginRequiredMixin,generic.DetailView):
model = Company
def get_context_data(self, **kwargs):
context = super(CompanyDetailView, self).get_context_data(**kwargs)
company_instance = self.object
context['histoire'] = company_instance.history.all()
return context
In my template,
<p>
Previous versions:
{% for item in histoire %}
<li>
{{ item }} submitted by {{ item.history_user }} {{
item.history_object }}
</li>
{% endfor %}
</p>
But ideally I want item.history_object to be a link that users can view the previous object and be able to revert if desired.
I did something similar by adding HistoricForm to my model forms.
class MyModelForm(HistoricForm, ModelForm):
...
HistoricForm takes and extra history_id kwarg.
If history_id is provided HistoricForm swaps the ModelForm instance with the historic_instance (what your instance looked like at the time of history_id). This way your form will show the historic version of your object.
class HistoricForm(object):
def __init__(self, *args, **kwargs):
self.history_id = kwargs.pop('history_id', None)
instance = kwargs.get('instance')
if instance and self.history_id:
kwargs['instance'] = self.get_historic_instance(instance)
super(HistoricForm, self).__init__(*args, **kwargs)
def get_historic_instance(self, instance):
model = self._meta.model
queryset = getattr(model, 'history').model.objects
historic_instance = queryset.get(**{
model._meta.pk.attname: instance.pk,
'history_id': self.history_id,
}).instance
return historic_instance
If history_id is not provided the ModelForm works as usual.
You can revert by showing the historic instance and save (this way you will post your historic data).

Passing Parameters to Django CreateView

I am trying to implement an appointment-making application where users can create sessions that are associated with pre-existing classes. What I am trying to do is use a django CreateView to create a session without asking the user for an associated class, while under the hood assigning a class to the session. I am trying to do this by passing in the pk of the class in the url, so that I can look up the class within the CreateView and assign the class to the session.
What I can't figure out is how exactly to do this. I'm guessing that in the template I want to have something like <a href="{% url create_sessions %}?class={{ object.pk }}>Create Session</a> within a DetailView for the class, and a url in my urls.py file containing the line
url(r'^create-sessions?class=(\d+)/$', CreateSessionsView.as_view(), name = 'create_sessions'), but I'm pretty new to django and don't exactly understand where this parameter is sent to my CBV and how to make use of it.
My plan for saving the class to the session is by overriding form_valid in my CBV to be:
def form_valid(self, form):
form.instance.event = event
return super(CreateSessionsView, self).form_valid(form)
If this is blatantly incorrect please let me know, as well.
Thank you!
GET parameters (those after ?) are not part of the URL and aren't matched in urls.py: you would get that from the request.GET dict. But it's much better to make that parameter part of the URL itself, so it would have the format "/create-sessions/1/".
So the urlconf would be:
url(r'^create-sessions/(?P<class>\d+)/$', CreateSessionsView.as_view(), name='create_sessions')
and the link can now be:
Create Session
and now in form_valid you can do:
event = Event.objects.get(pk=self.kwargs['class'])
urls.py
path('submit/request/<str:tracking_id>', OrderCancellationRequest.as_view(), name="cancel_my_order"),
Template
<form method="POST">
{% csrf_token %}
{{form | crispy}}
<button class="btn" type="submit">Submit</button>
</form>
View
class MyView(CreateView):
template_name = 'submit_request.html'
form_class = MyForm
model = MyModel
def form_valid(self, form, **kwargs):
self.object = form.save(commit=False)
self.object.created_at = datetime.datetime.now()
self.object.created_for = self.kwargs.get('order_id')
self.object.submitted_by = self.request.user.email
super(MyView, self).form_valid(form)
return HttpResponse("iam submitted")
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
context['header_text'] = "My Form"
context['tracking_id'] = self.kwargs.get('order_id')
return context

Dynamically get template name in bound Django form

I am trying to render a Django contact form on any arbitrary page. I am doing it with a request context processor and a template include. This allows me to display the form fine anywhere I want. Then I have a special URL that accepts POST requests (on GET, I just redirect them). If the form is valid, I send an email, and redirect to a success page. On form invalid, I know to pass the form bound with errors, but...I don't know which template to specify because the form is an include and the parent template could be anywhere.
The only way to get something in a Django view is from the request. I can get the path, and with more work, probably the original view from where the POST came from, but that doesn't get me the template.
# urls.py
url(r'^services/$', 'website.views.services', name='services'),
url(r'^services/contact/$', 'website.views.services_contact', name='services_contact'),
url(r'^services/contact/done/$', 'website.views.services_contact_done', name='services_contact_done')
# views.py
class ServicesView(TemplateView):
template_name = 'services/services.html'
services = ServicesView.as_view()
class ServicesContactView(View):
def get(self, request, *args, **kwargs):
return redirect('services')
def post(self, request, *args, **kwargs):
form = ContactForm(request.POST)
if form.is_valid():
form.send_email()
return redirect('services_contact_done')
else:
return render(request, ????, {'contact_form': form})
services_contact = ServicesContactView.as_view()
# contact.html
<h2>Contact me</h2>
<p>Enter your email to receive your questionnaire</p>
<form action="{% url 'services_contact' %}" method="post">
{% csrf_token %}
{% if contact_form.non_field_errors %}
{{ contact_form.non_field_errors }}
{% endif %}
{{ contact_form.as_p }}
<button type="submit" name="submit">Send questionnaire</button>
</form>
# home.html
{% extends "base.html" %}
{% block content %}
<h1>{{ site.name }}</h1>
{% include "services/contact.html" %}
{% endblock %}
The typical Django form view is somewhat silent on form invalid in that its scenario is mostly similar to an unbound form, so it's all just render in the end. My scenario is different due to the template include.
You could set up a session variable every time you render a template and use it afterwards when you need it :
request.session['template']="nameOfTemplate"
.
return render(request, request.session.get('template', 'default.html'), {'contact_form': form})
I know it requires to write a line of code every time you render a template, but that's the best solution I could think of.
If anybody needs this answer, I figured it out on my own. It's possible, but a different approach is required. First, a request context processor is not appropriate for this situation. They're fairly dumb because they just get something once and stick it in the context. Their only advantage is their global nature.
My context processor:
def contact_form(request):
"""
Gets the contact form and adds it to the request context.
You almost certainly don't want to do this.
"""
form = ContactForm()
return {'contact_form': form}
The nature of forms is that they act differently after being processed by Django's validation machinery, specifically ContactForm() is an unbound form and will always be. You don't want to do this (unless you want a form that simply displays but doesn't work). The TEMPLATE_CONTEXT_PROCESSORS should be edited to remove this processor.
Now the burden on displaying the form is back on the view, which also means just about any view must be able to handle POST requests as well. This means that editing each view that wants a contact form is required, but we can use the power of class-based views and mixins to handle most of the repetition.
ServicesView remains almost the same as a TemplateView, except with a mixin that will handle the form. This way, the template name always remains the same (my original problem), but with additional form power.
class ServicesView(ContactMixin, TemplateView):
template_name = 'services/services.html'
services = ServicesView.as_view()
ContactMixin uses FormMixin, to create and display a form, and ProcessFormView to handle the GET and POST requests for the form. And because the form's nature changes with different kinds of requests (unsubmitted, submitted and invalid, submitted and valid), get_context_data needs to be updated with the correct form class instance. Lastly, we probably want to prefix (namespace) our form because it can can be used anywhere, and we want to avoid conflicts when another possible form can POST to the same view. Thus, the mixin is:
class ContactMixin(FormMixin, ProcessFormView):
form_class = ContactForm
success_url = reverse_lazy('contact_done')
def get_form_kwargs(self):
kwargs = super(ContactMixin, self).get_form_kwargs()
kwargs['prefix'] = 'contact'
return kwargs
def get_context_data(self, **kwargs):
context = super(ContactMixin, self).get_context_data(**kwargs)
form_class = self.get_form_class()
context['contact_form'] = self.get_form(form_class)
return context
def form_valid(self, form):
form.send_email()
return super(ContactMixin, self).form_valid(form)
The subtleties of self.get_form_class() were almost lost on me if it were not for an example in the docs (of what not to do, heh) and another StackOverflow answer, where I would've usually just said self.form_class, which ignores the processing of the form.
Now I simply add ContactMixin to any view and {% include "includes/contact.html" %} to any template.