- 给表单字段指定class
- django 后台参数传给select下拉框
- admin新增页面的外键下拉框返回筛选后的值
- admin新增页面的外键下拉框可搜索
- 更改model在admin中的名字(后台管理表名称改中文)
- admin界面无法搜索外键字段
- error at line 0 错误| no such column:表名称.resource
- django多对多标签渲染、多选 select2
- django多对多表单的保存
- ListView实现搜索和搜索后正常分页|ListView使用context|get_queryset使用request
- django禁用formset,form表单集和表单的字段
- django/js 增加表单只复制元素,不复制值
- django获取数据表中某一列的值并转为list
- django使用多个不同formset,动态增加表单
- ListView中使用django自带的分页器paginator、排序
- django 有外键关系的表单集formset保存,内联表单集
- 外键关系的表单保存
- ‘A’ must be a ‘B’ instance
给表单字段指定class
1.从模型生成的表单
from django.forms import ModelForm,TextInput,CheckboxInputclass ContactForm(ModelForm):class Meta:model = Contactexclude = ['supplier']widgets = {'name': TextInput(attrs={'class': 'form-control', 'placeholder': "联系人"}),'manage': CheckboxInput(attrs={'class': 'form-check-input'}),}
2.自定义的表单
from django.forms import Form,TextInputclass UserForm(Form):username = forms.CharField(label="联系人",widget=TextInput(attrs={'class': 'form-control'}))
3.类似form.DELETE这种自动生成的部件
只能用js来修改
<script>$(document).ready(function() {let deleteForms = document.querySelectorAll("input[id$='-DELETE']")for(i = 0; i < deleteForms.length; i++){deleteForms[i].setAttribute('class','form-check-input')}});</script>
4.其他
class ContactForm(ModelForm):class Meta:model = Contactdef __init__(self, *args, **kwargs):super(ContactForm, self).__init__(*args, **kwargs)instance = getattr(self, 'instance', None)if instance and instance.pk:self.fields['name'].readonly = Trueself.fields['industries'].widget.attrs.update({'class':"js-example-basic-multiple"})
django 后台参数传给select下拉框
<select name="industry" class="js-example-basic-multiple" multiple="multiple">{% for obj in industries %}<option value="{{ obj.pk }}">{{obj}}</option>{% endfor %}
admin新增页面的外键下拉框返回筛选后的值
class AuthorityAdmin(admin.ModelAdmin):model = Authoritydef render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):# 替换下拉框中的值context['adminform'].form.fields['user'].queryset = Users.objects.filter()return super(AuthorityAdmin, self).render_change_form(request, context,add, change, form_url, obj)def formfield_for_foreignkey(self,db_field,request,**kwargs):# 或者用这个,两个都能用try:if db_field.name == 'user':kwargs['queryset'] = Users.objects.filter()return super(AuthorityAdmin,self).formfield_for_foreignkey(db_field,request,**kwargs)except Exception as e:print(e)
不能筛选后的值可搜索的原因:
https://stackoverflow.com/questions/62682725/filter-queryset-for-foreign-key-in-django-autocomplete-fields
autocomplete_fields会调用外键的get_queryset(),get_search_results(),必须重写get_search_results()才能实现,但是这样外键的搜索就会被破坏。
admin新增页面的外键下拉框可搜索
设置autocomplete_fields为外键字段,同时外键还需要设置search_fields
models.py
class Authority(models.Model):#https://docs.djangoproject.com/zh-hans/3.1/topics/db/examples/one_to_one/user = models.OneToOneField("home.Users",to_field="user_ip", on_delete=models.CASCADE,unique=True)add = models.BooleanField(verbose_name='增',default=False)
admin.py
@admin.register(Users)class UserAdmin(admin.ModelAdmin):search_fields = ["username"]@admin.register(Authority)class AuthorityAdmin(admin.ModelAdmin):autocomplete_fields = ['user']
更改model在admin中的名字(后台管理表名称改中文)
class Industry(models.Model):class Meta:verbose_name_plural='行业'
admin界面无法搜索外键字段
报错:Related Field got invalid lookup: icontains
search_fields中使用双下划线
class AuthorityAdmin(admin.ModelAdmin):model = Authoritysearch_fields = ['user__username']admin.site.register(Authority,AuthorityAdmin)
error at line 0 错误| no such column:表名称.resource
django多对多标签渲染、多选 select2
forms.py
也包括保存
class SupplierForm(ModelForm):tags = ModelMultipleChoiceField(queryset=Tag.objects.all(),label='物料范围',required=False)class Meta:model = Supplierfields = '__all__'def __init__(self, *args, **kwargs):if kwargs.get('instance'):initial = kwargs.setdefault('initial',{})initial['tags'] = [t.pk for t in kwargs['instance'].tag_set.all()]ModelForm.__init__(self,*args,**kwargs)instance = getattr(self, 'instance', None)if instance and instance.pk:self.fields['tags'].widget.attrs.update({'class':"js-example-basic-multiple"})def save(self, commit=True):instance = ModelForm.save(self,False)old_save_m2m = self.save_m2mdef save_m2m():instance.tag_set.clear()instance.tag_set.add(*self.cleaned_data['tags'])self.save_m2m = save_m2mif commit:instance.save()self.save_m2m()return instance
html
<head><script src="https://code.jquery.com/jquery-3.2.1.min.js"></script><link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.8/css/select2.min.css" rel="stylesheet" /><script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.8/js/select2.min.js"></script><script>$(document).ready(function() {$('.js-example-basic-multiple').select2();});</script></head>
django多对多表单的保存
https://stackoverflow.com/questions/2216974/django-modelform-for-many-to-many-fields
重写了save方法,没有理解透,就不分析了。
主要功能:一个供应商增减多个标签。不含标签本身的增减。
ListView实现搜索和搜索后正常分页|ListView使用context|get_queryset使用request
主界面是供应商的列表,用了ListView,然后还想实现搜索
from django.views import genericfrom .models import Supplierclass DetailView(generic.ListView):template_name = 'supplier/overview.html'context_object_name = 'supplier'paginate_by = 20def get_queryset(self):if self.request:search_str = self.request.GET.get('search')else:search_str = Falseif search_str:sp_list = Supplier.objects.filter(supplier_name__contains=search_str)return sp_listelse:return Supplier.objects.all()def get_context_data(self, **kwargs):context = super(DetailView, self).get_context_data(**kwargs)if self.request:search_str = self.request.GET.get('search')context['search'] = search_strreturn context
搜索后的分页,用了一个土办法
{% if search %}<li><a href="{{ request.path }}?page=1&search={{search}}">首页</a>{% if page_obj.has_previous %}<a href="{{ request.path }}?page={{ page_obj.previous_page_number }}&search={{search}}">上一页</a></li>{% else %}<a>上一页</a>{% endif %}{% for i in page_obj.paginator.page_range %}<a href="{{ request.path }}?page={{ i }}&search={{search}}">{{ i }}</a>{% endfor %}{% if page_obj.has_next %}<a href="{{ request.path }}?page={{ page_obj.next_page_number }}&search={{search}}">下一页</a>{% else %}<a>下一页</a>{% endif %}<a href="{{ request.path }}?page={{ page_obj.paginator.num_pages }}&search={{search}}">尾页</a></li>{% else %}<li><a href="{{ request.path }}?page=1">首页</a>{% if page_obj.has_previous %}<a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">上一页</a></li>{% else %}<a>上一页</a>{% endif %}{% for i in page_obj.paginator.page_range %}<a href="{{ request.path }}?page={{ i }}">{{ i }}</a>{% endfor %}{% if page_obj.has_next %}<a href="{{ request.path }}?page={{ page_obj.next_page_number }}">下一页</a>{% else %}<a>下一页</a>{% endif %}<a href="{{ request.path }}?page={{ page_obj.paginator.num_pages }}">尾页</a></li>{% endif %}
django禁用formset,form表单集和表单的字段
新增供应商的时候不需要禁用表单的字段,但是修改的时候,有的东西就不能随便改了
对单个表单form禁用字段field
forms.py
from .models import Supplierclass SupplierForm(ModelForm):class Meta:model = Supplierfields = '__all__'def disable_field(self):self.fields['unique_id'].widget.attrs['readonly'] = True
也可以使用self.fields['unique_id'].disabled = True,但是设置disabled = True的话,尽管修改时能够正常显示,但是提交表单的时候不会提交disabled = True的字段,如果字段本身必填就会报错。
渲染页面前需要调用一下这个方法
views.py
def edit_sth(request,sp_id):sp = Supplier.objects.get(pk=sp_id)if request.method == 'GET':sp_form = SupplierForm(instance=sp)sp_form.disable_field()return render(request, 'supplier/edit.html', {'sp_form': sp_form})
对表单集formset禁用字段field
表单集禁用有两个思路,定义一个表单form,一个是在初始化的时候就禁用相应的字段,然后在定义表单集的时候引用这个form。另一个是给这个form定义一个类似上文的disable_field方法在传递表单集formset之前循环调用。
补充:还是得初始化时就禁用,否则,表单填错的时候返回的页面会导致禁用失效
法1 form初始化时禁用
form.py
from .models import Contactfrom django.forms import ModelFormclass ContactForm(ModelForm):class Meta:model = Contactexclude = ['supplier'] #supplier是外键,随便,这里不影响def __init__(self, *args, **kwargs):super(ContactForm, self).__init__(*args, **kwargs)instance = getattr(self, 'instance', None)if instance and instance.pk:self.fields['name'].disabled = True
views.py
from django.forms import inlineformset_factorydef edit_sth(request,sp_id):sp = Supplier.objects.get(pk=sp_id)ContactFormSet = inlineformset_factory(parent_model=Supplier,model=Contact,form=ContactForm, fields='__all__',max_num=10,extra=1)#都需要指定表单if request.method == 'GET':ct_formset = ContactFormSet(instance=sp,prefix='contacts')return render(request, 'supplier/edit.html', {'ct_formset': ct_formset,'sp_form': sp_form})
法2 循环调用方法
form.py
class ContactForm(ModelForm):class Meta:model = Contactexclude = ['supplier']def disable_sth(self):instance = getattr(self, 'instance', None)if instance and instance.pk:self.fields['mail'].disabled = Trueself.fields['phone'].widget.attrs['readonly'] = True
views.py
from django.forms import inlineformset_factorydef edit_sth(request,sp_id):sp = Supplier.objects.get(pk=sp_id)ContactFormSet = inlineformset_factory(parent_model=Supplier,model=Contact,form=ContactForm, fields='__all__',max_num=10,extra=1)#都需要指定表单if request.method == 'GET':ct_formset = ContactFormSet(instance=sp,prefix='contacts')for fo in ct_formset:fo.disable_sth()return render(request, 'supplier/edit.html', {'ct_formset': ct_formset})
django/js 增加表单只复制元素,不复制值
做修改界面时发现的一个问题,django生成的form表单中的标签都设置好了特定的值,用js动态增加表单,原理是先复制一个现有的表单,这样会连表单的值一起复制出来。例如一个标签<input type="text" name="contacts-0-name" value="Q1" maxlength="20" id="id_contacts-0-name">,复制后会保留value="Q1"这个属性
一个方法是在views.py中设置extra属性,数量不定,在复制的时候选取倒数第一个表单即可,即使倒数的表单填写了,也不会增加value属性。
另一个方法就是把所有新增的显示的input标签找出来,删除value属性。
还有一个需要注意的问题,多对一关系进行修改,django生成的html页面会包含隐藏的input标签,这个标签的值是多对一关系的主键,例如,李四是供应商A的联系人,在数据库中保存这个联系人关系的主键id是23,那么会有一个隐藏的标签长这样:<input type="hidden" name="contacts-0-id" value="23" id="id_contacts-0-id">,如果初始复制的是这条记录对应的表单,那么等于关系也被复制过去,点击保存就会报错。解决方法就是把这个input用getElementsByName抓出来,setAttribute把value的值删掉即可。
django获取数据表中某一列的值并转为list
查了好久,其实只要一个list就能解决读取出来的是QuerySet的问题
tag_list = list(Tag.objects.values_list('tag_name',flat=True))
django使用多个不同formset,动态增加表单
(1) 使用单个formset参考:https://www.brennantymrak.com/articles/django-dynamic-formsets-javascript
(2) views.py中使用多个formset:https://docs.djangoproject.com/zh-hans/3.1/topics/forms/formsets/#using-more-than-one-formset-in-a-view
使用参数
prefix来给formset中表单的字段附上前缀,以避免多个formset的数据传到同一个视图而引起名称冲突
由于使用了多个不同的formset,首先在views.py中给表单集formset添加前缀prefix。
views.py
from django.forms import modelformset_factoryfrom .models import Contact,BankAccountfrom .forms import SupplierFormdef add_formset(request):ContactFormSet = modelformset_factory(Contact, exclude=('supplier',),max_num=10,extra=1)BankAccountFormSet = modelformset_factory(BankAccount, exclude=('supplier',),max_num=10,extra=1)if request.method == 'POST':sp_form = SupplierForm(request.POST)ct_formset = ContactFormSet(request.POST, request.FILES,prefix='contacts')ba_formset = BankAccountFormSet(request.POST, request.FILES,prefix='bankaccounts')state = all([sp_form.is_valid(),ct_formset.is_valid(),ba_formset.is_valid()])if state:q = sp_form.save()formsets = ct_formset.save(commit=False)for formset in formsets:formset.supplier = qformset.save()ba_formsets = ba_formset.save(commit=False)for formset in ba_formsets:formset.supplier = qformset.save()return HttpResponse('done!')else:return render(request, 'supplier/add.html', {'ct_formset': ct_formset,'ba_formset':ba_formset,'sp_form': sp_form})else:sp_form = SupplierForm()ct_formset = ContactFormSet(queryset=Contact.objects.none(),prefix='contacts')ba_formset = BankAccountFormSet(queryset=BankAccount.objects.none(),prefix='bankaccounts')return render(request, 'supplier/add.html', {'ct_formset': ct_formset,'ba_formset':ba_formset,'sp_form': sp_form})
这里直接在add_formset函数中定义了两个formset,也可以在项目的forms.py中定义。
传入传出时都需要添加prefix以区分这两个formset。
add.html
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>新增供应商</title></head><body><form id="form-container" method="POST">{% csrf_token %}<div>{{sp_form}}</div>{{ct_formset.management_form}}{% for form in ct_formset %}<div class="contact-form">{{form.as_p}}</div>{% endfor %}<button id="add-form" type="button">Add Another Contactor</button>{{ba_formset.management_form}}{% for baform in ba_formset %}<div class="ba-form">{{baform.as_p}}</div>{% endfor %}<button id="add-ba-form" type="button">Add Another BankAccount</button><input type="submit" value="Save"><script type="text/javascript">let birdForm = document.querySelectorAll(".contact-form")let container = document.querySelector("#form-container")let addButton = document.querySelector("#add-form")let totalForms = document.querySelector("#id_contacts-TOTAL_FORMS")let formNum = birdForm.lengthaddButton.addEventListener('click', addForm)function addForm(e){e.preventDefault()let newForm = birdForm[formNum-1].cloneNode(true)let formRegex = RegExp(`contacts-(\\d){1}-`,'g')newForm.innerHTML = newForm.innerHTML.replace(formRegex, `contacts-${formNum}-`)container.insertBefore(newForm, addButton)totalForms.setAttribute('value', `${formNum+1}`)ct_id = document.getElementsByName(`contacts-${formNum}-id`)ct_id[0].setAttribute('value','')}let baForm = document.querySelectorAll(".ba-form")let add_baButton = document.querySelector("#add-ba-form")let totalbaForms = document.querySelector("#id_bankaccounts-TOTAL_FORMS")let baformNum = baForm.lengthadd_baButton.addEventListener('click', add_baForm)function add_baForm(e){e.preventDefault()let newbaForm = baForm[baformNum-1].cloneNode(true)let baformRegex = RegExp(`bankaccounts-(\\d){1}-`,'g')newbaForm.innerHTML = newbaForm.innerHTML.replace(baformRegex, `bankaccounts-${baformNum}-`)container.insertBefore(newbaForm, add_baButton)totalbaForms.setAttribute('value', `${baformNum+1}`)ba_id = document.getElementsByName(`bankaccounts-${baformNum}-id`)ba_id[0].setAttribute('value','')}</script></form></body></html>
- 首先
{{xxformset.management_form}}是必需添加的,这部分会在网页中渲染出四个隐藏的input标签,只需要用到其中一个id_prefix-TOTAL_FORMS,里面的prefix就是先前设置的前缀。 - 由于两个formset都需要点击增加表单,
<script>这里要复制一下,改下变量名。整个function的大概逻辑就是,从现有的表单中复制一个新的表单,正则匹配id含有prefix的标签,由于新的表单连id都完全相同,所以把数量标志加1后替换,比如原先是id="prefix-0-title",现在变成id="prefix-1-title"。最后把这个新表单插入到按钮的上方。 - 还有一个问题就是,这种方法会导致formset预设的max_num不起作用,只要一直点就能一直增加,并且符合格式的填写都会写入数据库。
- 还有一个问题,id是隐藏的,但提交的时候还会照常提交,并且复制的时候还会把id相关的隐藏input一起复制过来,所以新增的输入框还要把id值去掉,不然会有bug。具体就是找到
prefix-${baformNum}-id这个元素,然后把值设为0ListView中使用django自带的分页器paginator、排序
views.py
```python from django.views import generic
class DetailView(generic.ListView): template_name = ‘supplier/overview.html’ context_object_name = ‘supplier’ #模型数据传入模板的名称 paginate_by = 10 #10条一页 ordering = [‘-id’] def get_queryset(self): return Supplier.objects.all()
<a name="ku39y"></a>### overview.html```html<ul ><li><a href="{{ request.path }}?page=1">首页</a>{% if page_obj.has_previous %}<a href="{{ request.path }}?&page={{ page_obj.previous_page_number }}">上一页</a></li>{% else %}<a>上一页</a>{% endif %}{% for i in page_obj.paginator.page_range %}<a href="{{ request.path }}?page={{ i }}">{{ i }}</a>{% endfor %}{% if page_obj.has_next %}<a href="{{ request.path }}?page={{ page_obj.next_page_number }}">下一页</a>{% else %}<a>下一页</a>{% endif %}<a href="{{ request.path }}?page={{ page_obj.paginator.num_pages }}">尾页</a></li></ul>
django 有外键关系的表单集formset保存,内联表单集
处理这类有外键关系的模型有个专门的方法,不多赘叙
https://docs.djangoproject.com/zh-hans/3.1/topics/forms/modelforms/#inline-formsets
views.py
from django.forms import inlineformset_factorydef edit_sth(request,sp_id):sp = Supplier.objects.get(pk=sp_id)ContactFormSet = inlineformset_factory(parent_model=Supplier,model=Contact,fields='__all__',max_num=10,extra=1)#有外键关系的formset初始化不太一样if request.method == 'GET':sp_form = SupplierForm(instance=sp)ct_formset = ContactFormSet(instance=sp,prefix='contacts')return render(request, 'supplier/edit.html', {'ct_formset': ct_formset,'sp_form': sp_form})if request.method == 'POST':sp_form = SupplierForm(request.POST,instance=sp)ct_formset = ContactFormSet(request.POST, request.FILES,instance=sp,prefix='contacts')state = all([sp_form.is_valid(),ct_formset.is_valid()])if state:sp_form.save()ct_formset.save()return HttpResponse('done!')else:return render(request, 'supplier/edit.html', {'ct_formset': ct_formset,'sp_form': sp_form})
外键关系的表单保存
‘A’ must be a ‘B’ instance
概述
在模型中定义了两张表,一张是描述供应商的Supplier,一张是描述联系人的Contact,存在多对一的关系,即多个联系人对应一个供应商。在表单提交的时候会有保存的问题。
models.py
from django.db import modelsfrom django.core.validators import MinLengthValidatorclass Supplier(models.Model):unique_id = models.CharField(verbose_name='社会统一信用代码',max_length=18,validators=[MinLengthValidator(18)],unique=True)supplier_name = models.CharField(verbose_name='供应商名',max_length=100)short_name = models.CharField(verbose_name='简称',max_length=20,blank=True,null=True)address = models.CharField(verbose_name='地址',max_length=200,blank=True,null=True)remarks = models.CharField(verbose_name='备注',max_length=200,blank=True,null=True)def __str__(self):return self.supplier_nameclass Contact(models.Model):#https://docs.djangoproject.com/zh-hans/3.1/topics/db/examples/many_to_one/name = models.CharField(verbose_name='联系人',max_length=20)supplier = models.ForeignKey(Supplier,to_field="unique_id", on_delete=models.CASCADE)phone = models.CharField(verbose_name='电话',max_length=20)
forms.py
from .models import Supplier,Contact,Tag,BankAccountfrom django.forms import ModelFormclass SupplierForm(ModelForm):class Meta:model = Supplierfields = '__all__'class ContactForm(ModelForm):class Meta:model = Contactexclude = ['supplier']
ContactForm剔除了supplier字段(exclude = ['supplier']),因为supplier字段是外键,包含进去的话,在html中会生成一个元素来选择对应的供应商,这里会有问题,在新增供应商时需要对应新增联系人,但表单是一起提交的,只有保存表单后,新增的供应商才会存到数据库内,从而被访问选择。并且,新增的时候肯定是指定了增加这个新增供应商对应的联系人,而不是其他供应商的,所以剔除了supplier。
两个表单都是从模型生成的,参考:https://docs.djangoproject.com/zh-hans/3.1/topics/forms/modelforms/
views.py
from django.shortcuts import renderfrom django.http import HttpResponsefrom .forms import SupplierForm,ContactFormdef add_sth(request):# if this is a POST request we need to process the form dataif request.method == 'POST':sp_form = SupplierForm(request.POST)ct_form = ContactForm(request.POST)if sp_form.is_valid():q = sp_form.save() #此时q就是Supplier类型的实例print(type(q)) #<class 'supplier.models.Supplier'>print(type(sp_form)) #<class 'supplier.forms.SupplierForm'>print(type(sp_form.cleaned_data)) #<class 'dict'>print(type(sp_form.cleaned_data['unique_id'])) #<class 'str'>ct_temp = ct_form.save(commit=False)ct_temp.supplier = sp_form.cleaned_data['unique_id'] #此处报错ct_temp.supplier = q #这样就没问题了ct_temp.save()return HttpResponse('finish!')# if a GET (or any other method) we'll create a blank formelse:sp_form = SupplierForm()ct_form = ContactForm()return render(request, 'supplier/add.html', {'sp_form': sp_form,'ct_form':ct_form})
由于模型定义中,外键supplier是必需的,所以表单保存时如果缺少外键,写入数据库时会报错。因此要设置save()方法的属性commit=False,设置后ct_form会保存到ct_temp,但不会写入数据库。
此时对ct_temp的supplier字段赋值,由于模型定义时绑定的是unique_id字段,所以从sp_form中取出unique_id。
报错
逻辑上好像也没啥问题,但还是报错了Cannot assign "'914300007607233766'": "Contact.supplier" must be a "Supplier" instance.914300137607233766是随便填的unique_id,由于报错时是一对双引号套了一对单引号,我一度以为是取出的unique_id类型有问题,毕竟正常显示话应该只有一对单引号,但实际上这里没问题,报错的格式是"赋值内容",由于unique_id是str类型,所以双引号套个单引号没问题。"Contact.supplier" must be a "Supplier" instance.此处才是问题所在,赋值给ct_temp.supplier的应该是一个Supplier实例。归根到底这里的表单不是直接写入数据库的,所以unique_id是不行的。
add.html
<form method="POST">{% csrf_token %}
<div>
{{sp_form}}
</div>
<div>
{{ct_form}}
</div>
<input type="submit" value="Save">
</form>
两个表单sp_form和ct_form得放在<form>标签内,否则不能提交
