Django admin - inline inlines (or, three model editing at once) - python

I've got a set of models that look like this:
class Page(models.Model):
title = models.CharField(max_length=255)
class LinkSection(models.Model):
page = models.ForeignKey(Page)
title = models.CharField(max_length=255)
class Link(models.Model):
linksection = models.ForeignKey(LinkSection)
text = models.CharField(max_length=255)
url = models.URLField()
and an admin.py that looks like this:
class LinkInline(admin.TabularInline):
model = Link
class LinkSectionInline(admin.TabularInline):
model = LinkSection
inlines = [ LinkInline, ]
class PageAdmin(admin.ModelAdmin):
inlines = [ LinkSectionInline, ]
My goal is to get an admin interface that lets me edit everything on one page. The end result of this model structure is that things are generated into a view+template that looks more or less like:
<h1>{{page.title}}</h1>
{% for ls in page.linksection_set.objects.all %}
<div>
<h2>{{ls.title}}</h2>
<ul>
{% for l in ls.link_set.objects.all %}
<li>{{l.title}}</li>
{% endfor %}
</ul>
</div>
{% endfor %}
I know that the inline-in-an-inline trick fails in the Django admin, as I expected. Does anyone know of a way to allow this kind of three level model editing? Thanks in advance.

You need to create a custom form and template for the LinkSectionInline.
Something like this should work for the form:
LinkFormset = forms.modelformset_factory(Link)
class LinkSectionForm(forms.ModelForm):
def __init__(self, **kwargs):
super(LinkSectionForm, self).__init__(**kwargs)
self.link_formset = LinkFormset(instance=self.instance,
data=self.data or None,
prefix=self.prefix)
def is_valid(self):
return (super(LinkSectionForm, self).is_valid() and
self.link_formset.is_valid())
def save(self, commit=True):
# Supporting commit=False is another can of worms. No use dealing
# it before it's needed. (YAGNI)
assert commit == True
res = super(LinkSectionForm, self).save(commit=commit)
self.link_formset.save()
return res
(That just came off the top of my head and isn't tested, but it should get you going in the right direction.)
Your template just needs to render the form and form.link_formset appropriately.

Django-nested-inlines is built for just this. Usage is simple.
from django.contrib import admin
from nested_inlines.admin import NestedModelAdmin, NestedStackedInline, NestedTabularInline
from models import A, B, C
class MyNestedInline(NestedTabularInline):
model = C
class MyInline(NestedStackedInline):
model = B
inlines = [MyNestedInline,]
class MyAdmin(NestedModelAdmin):
pass
admin.site.register(A, MyAdmin)

My recommendation would actually be to change your model. Why not have a ForeignKey in Link to LinkSection? Or, if it's not OneToMany, perhaps a ManyToMany field? The admin interface will generate that for free. Of course, I don't recommend this if links don't logically have anything to do with link sections, but maybe they do? If they don't, please explain what the intended organization is. (For example, is 3 links per section fixed or arbitrary?)

You can create a new class, similar to TabularInline or StackedInline, that is able to use inline fields itself.
Alternatively, you can create new admin templates, specifically for your model. But that of course overrules the nifty features of the admin interface.

Related

How to replace Foreign Key with Many To Many in Django models without broking whole app?

I want to change my Foreign Key to Many To Many field to let the user select multiple categories in a dropdown list.
This is what I already have. After I change Foreign Key to Many To Many I'm getting milion errors, I have to get rid of on_delete=models.CASCADE which is a core of my app. What can I do? Which way should I take? Maybe add another model? I'm so confused, especially when I am a Django newbie. Thank you for your help!
MODELS
class Category(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return f'{self.name}'
class Expense(models.Model):
class Meta:
ordering = ('date', '-pk')
category = models.ForeignKey(Category, null=True,blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
amount = models.DecimalField(max_digits=8,decimal_places=2)
date = models.DateField(default=datetime.date.today,db_index=True)
def __str__(self):
return f'{self.date} {self.name} {self.amount}'
The clue of the application is to let the user create a category e.g "PC". Then add some expenses like "GPU", "CPU" etc... and let the user link it to the "PC" category. And when the user wants to delete certain categories, all the expenses linked to it, gonna be deleted too. And this is the thing I have already did. BUT NOW I want to let the user search the main table of expenses by multiple categories. And here comes my problem, I don't have a clue how to do it and keep the whole application in one piece with all the functionalities.
SCREENSHOTS:
Categories View with just added PC category
Expense Add View
I don't think there is a simple answer to your question, but here are some resources that might help. First, I don't think you should change your models. From the way you described your application, I think a foreign key model with on_delete=CASCADE is good. The basic idea here is that you need to change your list view function so that it performs a query of your database. Also modify your template.html to include a search bar.
https://github.com/csev/dj4e-samples/tree/master/well
https://www.dj4e.com/lessons/dj4e_ads4
Modify Your List View To Allow The Searching
This is an example of a list view that allows you to search for a single term, and returns anything in the database that matches from any field. This isn't what you want to do exactly, but if you can get this working then you can modify the search conditions for your specific application. What is going on in the code below is that instead of return every item in my Ad table in my SQL database, I filter it based on the search. Then, I pass "ad_list" to the template view. Since I already filtered ad_list based on the search, in the template view it will only list the items that match. This is based on the DJ4E course, and you can watch the video there to get an idea of how he implements the search bar better.
from ads.models import Ad
from django.views import View
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse_lazy, reverse
from django.http import HttpResponse
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.contrib.humanize.templatetags.humanize import naturaltime
from ads.utils import dump_queries
from django.db.models import Q
class AdListView(ListView):
# By convention:
template_name = "ads/ad_list.html"
def get(self, request) :
strval = request.GET.get("search", False)
if strval :
# Simple title-only search
# objects = Ad.objects.filter(title__contains=strval).select_related().order_by('-updated_at')[:10]
# Multi-field search
query = Q(title__contains=strval)
query.add(Q(text__contains=strval), Q.OR)
objects = Ad.objects.filter(query).select_related().order_by('-updated_at')[:10]
else :
# try both versions with > 4 posts and watch the queries that happen
objects = Ad.objects.all().order_by('-updated_at')[:10]
# objects = Ad.objects.select_related().all().order_by('-updated_at')[:10]
# Augment the post_list
for obj in objects:
obj.natural_updated = naturaltime(obj.updated_at)
ctx = {'ad_list' : objects, 'search': strval}
retval = render(request, self.template_name, ctx)
dump_queries()
return retval;
Modify Your Template.html to include a search bar
<form>
<input type="text" placeholder="Search.." name="search"
{% if search %} value="{{ search }}" {% endif %}
>
<button type="submit"><i class="fa fa-search"></i></button>
<i class="fa fa-undo"></i>
</form>
PS, I think you can answer your own question better when you figure it out, so help others and post it!

Django ModelForm iterate through MultipleChoiceField Values and process on POST

Level: Absolute Beginner, trying to build an app to perform some db operation through web UI
models.py
from django.db import models
class MysqlUser(models.Model):
username = models.CharField(max_length=100)
password = models.CharField(max_length=50)
environment = models.CharField(max_length=50)
forms.py
from django import forms
from onboard_app.models import MysqlUser
class MysqlUserForm(forms.ModelForm):
CHOICES = (
('test', 'Test'),
('develop', 'Develop'),
('staging', 'Staging'),
)
environment = forms.MultipleChoiceField(choices=CHOICES)
password = forms.CharField(widget=forms.PasswordInput)
class Meta:
model = MysqlUser
fields = ('username', 'password', 'environment')
views.py
from django.shortcuts import render
from onboard_app.serializers import MysqlUserSerializer
from rest_framework import generics
from onboard_app.forms import MysqlUserForm
from onboard_app.models import MysqlUser
from django.views.generic.edit import CreateView, UpdateView, DeleteView
class MysqlCreateView(CreateView):
model = MysqlUser
form_class = MysqlUserForm
template_name = 'mysqluser_form.html'
success_url = '/mysql/user/add/'
mysqluser_form.html
{% extends "myapp/base.html" %}
{% block title %}MyApp{% endblock %}
{% block content %}
<h1>MySQL User Access</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" class="btn btn-primary" value="Grant Access">
</form>
{% endblock %}
I'm trying to get Value(s) of field or MultipleChoiceFiled environment after the user Form Submit, and loop through the entered values to perform some action. I have been trying this for long, still can't figure out how. I don't want to process anything in the frontend html. I'm thinking it has to be processed in the Views but not sure how to get the values of the field and loop over.
Any examples or any django concepts to look into will help me a lot. Any help is appreciated, thanks in advance!
Unless you have already done so, I recommend going through Django docs on class-based views (https://docs.djangoproject.com/en/3.0/topics/class-based-views/intro/) to get an overview of how the whole thing works.
So after you submit a form, a post() method of your view is called. CreateView provides a default implementation, which validates the user input using the MysqlUserForm you have provided and then creates an instance of MysqlUser and redirects to the success_url.
If you want to add more logic, you need to overwrite the post() method (or some other method, called by post(), in particular, the form_valid method) and put your logic there. To get a complete sense of how things work, I recommend you to read through the CreateView, BaseCreateView, ProcessFormView, and ModelFormMixin source code, although the inheritance looks a bit complicated (and it is). More to that, I really advise you to walk through the request processing from the View.dispatch method all way to the form_valid method using the debugger and see how things really work. Trust me, it will really contribute to your development skills improvement and understanding. Actually, I've discovered the form_valid method to write this answer by reading the source code (I use rest-framework nowadays and don't remember a lot about Django views).
So, what you need is
class MysqlCreateView(CreateView):
model = MysqlUser
form_class = MysqlUserForm
template_name = 'mysqluser_form.html'
success_url = '/mysql/user/add/'
def form_valid(self, form):
environment = form.cleaned_data['environment']
# insert your code here
return super().form_valid(form)
P.S. A good IDE like a PyCharm is really much much more convenient to read source code, jump to relevant parts and debug than any text editor and PDB debugger.

Problem with ManyToManyField in other model's forms using Bootstrap

I would like to show possible choices from ManyToManyField (which I have in Homes model) in the Owners form. I have Owners <--Many2Many--> Homes with custom class HomesOwners. In Homes it works out of the box, I don't know how to make it work in Owners.
I am using Django 2.2.4 with Bootstrap 4 and Postgresql. I started my project based on django-bookshelf project (also just Django and Bootstrap4). I do not use any render. Comment in django-bookshelf project mentioned How to add bootstrap class to Django CreateView form fields in the template?, so I stick to that if it comed to forms.
I'm pretty new to Python (so Django too) and web technologies in general. I googled dozen of different questions/answers but I couldn't find any nice explanation of what is what and how to use it in real life. Most of them ended up with basic usage.
I did some experimentation on my own, but no success so far...
Here is the code
I have two models - Homes/models.py and Owners/models.py
Homes/models.py:
class Homes(models.Model):
id = models.AutoField(primary_key=True)
# other fields
some_owners = models.ManyToManyField(Owners, through='HomesOwners', through_fields=('id_home', 'id_owner'), related_name='some_owners')
# end of fields, some other code in the class like "class Meta" etc.
class HomesOwners(models.Model):
id = models.AutoField(primary_key=True)
id_home = models.ForeignKey(Homes, models.DO_NOTHING, db_column='id_home')
id_owner = models.ForeignKey('owners.Owners', models.DO_NOTHING, db_column='id_owner')
Owners/models.py do not have anything special, no imports from my Homes/models.py etc.
and my forms:
Homes/forms.py:
class HomesForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(HomesForm, self).__init__(*args, **kwargs)
self.fields['some_field_from_homes_model'].widget.attrs = {'class': 'form-control '}
#
# --> no need self.fields for M2M, Django does the work
#
# but I tried also and have a --> Question 2
# self.fields["some_owners"].widget = forms.widgets.CheckboxSelectMultiple()
# self.fields["some_owners"].queryset = HomesOwners.objects.all()
Without any code as "self.fields" for M2M field, Django is able to generate for me list of owners.
Question 1
I would like to get list of Homes in my OwnersForms.
I do not know what to add. I assume that I cannot add
# Owners/models.py
some_homes = models.ManyToManyField(Homes, through='HomesOwners', through_fields=('id_home', 'id_owner'), related_name='some_homes')
because of circular import, am I right?
How do I get my Homes list using self.fields?
What do I need to add to my code?
Question 2
When I've added
# Homes/forms.py
self.fields["some_owners"].widget = forms.widgets.CheckboxSelectMultiple()
self.fields["some_owners"].queryset = HomesOwners.objects.all()
I got
<!-- html page -->
HomesOwners object (1)
HomesOwners object (2)
<!-- and so on... -->
How can I just list Owners?
How to filter/order them so first they would appear Owners not connected to any Home?
Question 3
class HomesOwners(models.Model):
id = models.AutoField(primary_key=True)
id_home = models.ForeignKey(Homes, models.DO_NOTHING, db_column='id_home')
id_owner = models.ForeignKey('owners.Owners', models.DO_NOTHING, db_column='id_owner')
def __str__(self):
return pass #return something
I can't get my head around this. This class connects Homes and Owners. When I'm thinking of Homes I would like to return Owners and vice versa. So it should return different things depending on what object we are using (home or owner). I think this is connected to my 2nd question about:
HomesOwners object (1)
Also...
In homes.html I'm using my M2M like that:
{% for owner in homes.homesowners_set.all %}
{{ owner.id_owner.id }}
{% endfor %}
I would like to write something similar to my owners.html and list homes. This is connected to my previous question, I would like to have full answer if that's possible.
EDIT
With the answer given to me I was able to add Homes to OwnerUpdate view. I have views like that:
owners/views.py
# List View
class OwnersList(ListView):
model = Owners
# Detail View
class OwnersView(DetailView):
model = Owners
# Create View
class OwnersCreate(CreateView):
model = Owners
form_class = OwnersForm
# Setting returning URL
success_url = reverse_lazy('owners_list')
# Update View
class OwnersUpdate(UpdateView):
model = Owners
form_class = OwnersForm
success_url = reverse_lazy('owners_list')
# Delete View
class OwnersDelete(DeleteView):
model = Owners
success_url = reverse_lazy('owners_list')
What change do I need to make to be able to show in OwnersList Homes they own?
In Homes DetailView I am able to show Owners. I would like to do the same for Homes' DetailView and Homes ListView.
I don't really get it what you asking for,
but if I understand your question correctly,
I assume you want to add Homes list (not HomesOwners list) into your Owners form, right?
you can add extra field in your form like this:
class OwnersForm(ModelForm):
# notice the queryset is 'Homes' not 'HomesOwners'
homes = forms.ModelMultipleChoiceField(queryset=Homes.objects.all(), widget=forms.CheckboxSelectMultiple)
class Meta:
model = Owners
fields = ('homes', 'your_other_fields',)
# then you can access in init function too if you want
def __init__(self, *args, **kwargs):
super(OwnersForm, self).__init__(*args, **kwargs)
self.fields['homes'].required = False
then, since it using CheckboxSelectMultiple widget, you can iterate it in your html template like this:
{% for key, value in your_form.homes.field.choices %} <!-- notice '.field.'-->
{{ key }} = {{ value }}
{% endfor %}
you probably need to create custom save too for your form.
for your question 3, it is not about the form?
If you want to show HomesOwners, you are already doing right.
{% for owner in homes.homesowners_set.all %}
{{ owner.id_owner.id }}
{% endfor %}
but it will work if that homes is only 1 object.
if homes is a queryset, you have to iterate it first
{% for home in homes %}
{% for owner in home.homesowners_set.all %}
{{ owner.id_owner.id }}
{% endfor %}
{% endfor %}
sorry if I misunderstanding your questions,
maybe you can provide your views.py too, so I or others can help you more specific

Link 2 fields from a model in django

I am relatively new to Django and I made a Todo list where user can add a task and mark if its completed. I added a form field of priorities which is a radio select widget. Based on the priority the task field will have red, orange or green color.
The radio buttons appear correctly and I cant post a task without giving an input priority. But the priority is always taken as default(high).
I tried a couple of things to change and display the priorities but nothing worked.
I believe something in the views.py is to be modified to make it work but due to my lack of experience I cannot put a finger on it.
Views.py
#require_POST
def addTodo(request):
form = TodoForm(request.POST)
#print(request.POST['text'])
if form.is_valid():
new_todo = Todo(text = request.POST['text'])
new_todo.save()
for item in form:
return redirect('index')
def completeTodo(request, todo_id):
todo = Todo.objects.get(pk=todo_id)
todo.complete = True
todo.save()
return redirect('index')
form.py
from django import forms
prior_choice =[('high','High'),('mod','Mod'),('low','Low')]
class TodoForm(forms.Form):
text = forms.CharField(max_length = 40,
widget = forms.TextInput(
attrs= {'class': 'form-control', 'placeholder': 'Enter todo e.g. Delete junk files', 'aria-label': 'Todo', 'aria-describedby':'add-btn'}))
priority = forms.CharField(widget=forms.RadioSelect(choices=prior_choice))
models.py
from django.db import models
class Todo(models.Model):
text = models.CharField(max_length=40)
complete = models.BooleanField(default = False)
task_priority = models.CharField(max_length=40, default='high')
def __str__(self):
return self.text
index.html
<ul class="list-group t20">
{% for todo in todo_list %}
{% if todo.task_priority == 'high'%}
{{ todo.text}}</li>
{%elif todo.task_priority == 'mod'%}
{{ todo.text}}</li>
{%elif todo.task_priority == 'low'%}
{{ todo.text}}</li>
{%else%}
<div class="todo-completed"> <li class="list-group-item" style="background-color: green;"> {{ todo.text}}</li></div>
{%endif%}
{% endfor %}
</ul>
Heres a screenshot of the output app
Please help me link the radio button to a task in the list and display accordingly.
Thanks in advance.
The problem is in your view. While you are creating your Todo object you are not passing the priority.
new_todo = Todo(text = request.POST['text'], task_priority = request.POST['priority'])
The code above solves your problem. But I DO NOT RECOMMEND it. You are not leveraging the Django forms. Please use Django forms.cleaned_data to get parameters instead of request.POST or use ModelForm which will allow you to save from form instance directly.
Model Change Advice
However this is not how i would like solve the issue. You can change your model as following to have more djangoic way of doing it:
from django.utils.translation import ugettext_lazy as _
class Todo(models.Model):
PRIORITY_NONE = 0
PRIORITY_LOW = 1
PRIORITY_MODERATE = 2
PRIORITY_HIGH = 3
PRIORITIES = (
(PRIORITY_NONE, _('')),
(PRIORITY_LOW, _('Low')),
(PRIORITY_MODERATE, _('Moderate')),
(PRIORITY_HIGH, _('High')),
)
...
task_priority = models.PositiveSmallIntegerField(choices=PRIORITIES, default=PRIORITY_NONE)
You may need to change your form with the choices Todo.PRIORITIES. Also you may want to use ModelForm which will make things much easier for you.

Django: efficient template/string separation and override

I have a generic Django view that renders a template. The template is in an app which other projects will use. Importing projects will typically subclass the View the app provides. The View has a default template, which does a job with generic wording.
99% of the time, subclassing Views will want to only change the text, so rather than make them duplicate the template for the sake of altering non-markup wording, i'm looking for a way to allow users of the class to replace wording in the template in the most efficient way.
Options explored so far:
template partials containing only the text which using apps can override (magic, a lot of user work)
A template_strings method on the view which provides a dict of strings which end up in the template context which subclasses can override
Using (abusing?) the translation system such that the app provides default english translations and using code can provide their own translations instead (not actually worked this one out yet, just an idea)
Doing the above template_strings through AppConfig, but this seems ... yucky like it may get very unweildy with a lot of English strings. If doing this I would create a context-like setup so you don't have to re-declare all strings
Seems like it should be a solved problem to subclass a view which does a complete job and just provide alternate strings for text. Is there a better method than the above? Convention? Something I am missing?
(django 1.11 Python 3.6.2)
You can either inherit TemplateView or add ContextMixin to your view, and then override the get_context_data function like this:
from django.views.generic import TemplateView
class BaseView(TemplateView):
template_name = "common.html"
class SubView(BaseView):
def get_context_data(self, **kwargs):
context = super(SubView, self).get_context_data(**kwargs)
context['content'] = "Some sub view text"
return context
Update: Use template overriding
If you want to separate the text out, this is the better way to go
To allow easily and DRY override template across apps, you might need to install this package (Some other detail here)
We define it similarly as above, but change the template_name instead:
from django.views.generic import TemplateView
class BaseView(TemplateView):
template_name = "main.html"
# on another app
class SubView(BaseView):
template_name = "sub_view.html"
Then the magic is you can extends and override block of the BaseView template like this:
base_app/templates/main.html
<p>I'm Common Text</p>
{% block main %}
<p>I'm Base View</p>
{% endblock %}
sub_app/templates/sub_view.html
{% extends "base_app:main.html" %}
{% block main %}
<p>I'm Sub View</p>
{% endblock %}
The result would be:
<p>I'm Common Text</p>
<p>I'm Sub View</p>
Afaik you covered the options pretty well. My example is probably just a variant of the the template strings but maybe it helps anyway...
class DefaultStringProvider():
TITLE = 'Hello'
DESCRIPTION = 'Original description'
CATEGORY = 'Stuff'
class MyTemplateBaseView(TemplateView):
def get_context_data(self, **kwargs):
return super(MyTemplateBaseView, self).get_context_data(
provider=self.get_string_provider(), **kwargs)
def get_string_provider(self):
return DefaultStringProvider()
class OtherView(MyTemplateBaseView):
template_name = 'welcome.html'
def get_string_provider(self):
p = DefaultStringProvider()
p.TITLE = 'Hello'
p.DESCRIPTION = 'New description'
return p
The idea is to have a default string provider and the base view populates the context with it through get_string_provider().
It will at least be quite clear which strings can be overridden for a user extending the base class and it will not interfere with translations.

Categories