How to modify values in m2m field in form? (Django) - python

I have 2 models in my models.py. They are Doctor and Specialty:
class Doctor(models.Model):
name = CharField(max_length=255)
specialties = ManyToManyField('Specialty', related_name='doctors')
mc_id = CharField(max_length=255, unique=True)
class Specialty(models.Model):
name = CharField(max_length=255)
mc_id = CharField(max_length=255, unique=True)
def __str__(self):
return self.name
When i'm creating new Doctor instance via admin interface (change_form.html template) I see my Doctor model fields, which I should fill with data. Among these fields there is a field specialties ("Cпециальности"), which is m2m field and contains names (as I set __str__(self) method in Specialty model to return self.name) for specialties which were previously added to Specialty model (see a screen).
All I want is to see in this list not only specialty names, but also values of mc_id field of Specialty model, but it is not my case to make it at the expense of something like this:
class Specialty(models.Model):
name = CharField(max_length=255)
mc_id = CharField(max_length=255, unique=True)
def __str__(self):
return self.name + '(' + self.mc_id + ')'
I thought i could make it through overriding of formfield_for_manytomany() method in my admin model (associated with Doctor model) in admin.py:
#admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "specialties":
queryset = Specialty.objects.all()
for specialty in queryset:
specialty.name += '(' + {specialty.mc_id} + ')'
return super().formfield_for_manytomany(db_field, request, **kwargs)
It didn't work. I'd like to know how can i solve my problem. Thank you in advance.
UPDATE: I'd also like to make a content of specialties field in the form user-dependent:
if superuser tries to add new doctor he will see in the specialties field of the form values like:
Internal Medicine (mc1_id)
Neurology (mc1_id)
Cardiology (mc2_id)
if normal user which name is 'mc1_id' tries to add new doctor he will see in the form values filtered by his name and without '(mc_id)'-part. Like:
Internal Medicine
Neurology

I think that overwrite the label_from_instance method in the form field can work for you.
class SpecialtyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
obj.name + '(' + obj.mc_id + ')'
class DoctorModelForm(forms.ModelForm):
specialties = SpecialtyModelMultipleChoiceField(
Specialty.objecs.all
# ....
)
class Meta:
model = Doctor
fields = '__all__'
#admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
form = DoctorModelForm
# ....
UPDATE:
In order to make the field text user dependent you can return different classes in the get_form method of the Admin Model:
class SpecialtyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
obj.name + '(' + obj.mc_id + ')'
class DoctorModelForm(forms.ModelForm):
specialties = SpecialtyModelMultipleChoiceField(
Specialty.objecs.all
# ....
)
class Meta:
model = Doctor
fields = '__all__'
class SimpleDoctorModelForm(forms.ModelForm):
class Meta:
model = Doctor
fields = '__all__'
#admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kw):
if request.user.is_superuser:
return DoctorModelForm
return SimpleDoctorModelForm
If you need more, you can set properties in the form and work with its in the constructor:
class DoctorModelForm(forms.ModelForm):
class Meta:
model = Doctor
fields = '__all__'
#classmethod
def set_user(cls, user):
cls.__user = user
def __init__(self, *args, **kw):
super(DoctorModelForm, self).__init__(*args, **kw)
if self.__user.condition:
self.fields['specialties'] = MyCustomField
#admin.register(Doctor)
class DoctorAdmin(admin.ModelAdmin):
form = DoctorModelForm
def get_form(self, request, obj=None, **kw):
form_class = super(DoctorAdmin, self).get_form(request, obj, **kw)
form_class.set_user(request.user)
return form_class

Just use format() in models
def __str__(self):
return '{} {}'.format(self.name, self.mc_id)

I'd like to share an altnernative realisation of Tonio's great idea.
# fields.py
class SpecialtyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
obj.name + '(' + obj.mc_id + ')'
# admin.py
class BaseAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
# some logic
return form
#admin.register(Doctor)
class DoctorAdmin(BaseAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "specialties":
queryset = Specialty.objects.all().filter(mc_id=request.user)
kwargs.update({'queryset': queryset})
return super().formfield_for_manytomany(db_field, request, **kwargs)
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if request.user.is_superuser:
form.base_fields['specialties'] = SpecialtyModelMultipleChoiceField(Specialty.objects.all())
return form
I think it can be useful in situations when DoctorAdmin is inherited from some base model like BaseAdmin and this base model also has overriden get_form() method. But here in case of superuser's request queryset is formed twice, therefore Tonio's answer is definetely better.

Related

Using inlineformset_factory with different querysets in CreateView

I am trying to use inlineformset_factory to create instances of the same model.
models.py
class Skill(models.Model):
employee = models.ForeignKey(
Employee, on_delete=models.CASCADE, related_name="employee_skills")
technology = models.ForeignKey(Technology, on_delete=models.CASCADE)
year = models.CharField('common year using amount ', max_length=4)
last_year = models.CharField('Last year of technology using ', max_length=4)
level = models.CharField("experience level", max_length=64, choices=LEVELS)
class Techgroup(models.Model):
""" Group of technology """
name = models.CharField('group_name', max_length=32, unique=True)
class Technology(models.Model):
"""Technologies."""
name = models.CharField('technology name', max_length=32, unique=True)
group = models.ForeignKey(Techgroup, on_delete=models.CASCADE, related_name="group")
In the Administrator pane I created 2 instances of the Techgroup model:
- Framework
- Programming language
All Skill models belong to one of two groups. On the front I display 2 forms, one containing queryset with instances belonging to the Framework, the other with instances belonging to the Programming language.
I divide Querysets using ModelsForm:
forms.py
class SkillBaseCreateForm(forms.ModelForm):
YEAR_CHOICES = [(r, r) for r in range(1, 11)]
LAST_YEAR_CHOICES = [(r, r) for r in range(2015, datetime.datetime.now().year + 1)]
year = forms.CharField(
widget=forms.Select(choices=YEAR_CHOICES),
)
last_year = forms.CharField(widget=forms.Select(choices=LAST_YEAR_CHOICES))
class Meta:
model = Skill
fields = ['technology', 'level', 'last_year', 'year']
class SkillCreatePLanguageForm(SkillBaseCreateForm):
def __init__(self, *args, **kwargs):
super(SkillCreatePLanguageForm, self).__init__(*args, **kwargs)
self.fields['technology'].queryset = Technology.objects.filter(group__name="Programming language")
class SkillCreateFrameworkForm(SkillBaseCreateForm):
def __init__(self, *args, **kwargs):
super(SkillCreateFrameworkForm, self).__init__(*args, **kwargs)
self.fields['technology'].queryset = Technology.objects.filter(group__name="Framework")
SkillFrameworkFormSet = inlineformset_factory(Employee, Skill, form=SkillCreateFrameworkForm, extra=1, can_delete=False)
SkillPLanguageFormSet = inlineformset_factory(Employee, Skill, form=SkillCreatePLanguageForm, extra=1, can_delete=False)
views.py
class SkillTestCreateView(AuthorizedMixin, CreateView):
"""
Create new skill instances
"""
template_name = 'edit.html'
model = Employee
form_class = EmployeeEditForm
def get(self, *args, **kwargs):
"""
Handles GET requests and instantiates blank versions of the form
and its inline formsets.
"""
self.object = Employee.objects.get(pk=self.kwargs['pk'])
form_class = self.get_form_class()
form = self.get_form(form_class)
form_framework = SkillFrameworkFormSet()
form_language = SkillPLanguageFormSet()
return self.render_to_response(
self.get_context_data(form=form,
form_framework=form_framework,
form_language=form_language))
def post(self, request, *args, **kwargs):
"""
Handles POST requests, instantiating a form instance and its inline
formsets with the passed POST variables and then checking them for
validity.
"""
self.object = Employee.objects.get(pk=self.kwargs['pk'])
form_class = self.get_form_class()
form = self.get_form(form_class)
form_framework = SkillFrameworkFormSet(self.request.POST)
form_language = SkillPLanguageFormSet(self.request.POST)
if (form.is_valid() and form_framework.is_valid() and
form_language.is_valid()):
return self.form_valid(form, form_framework, form_language)
else:
return self.form_invalid(form, form_framework, form_language)
def form_valid(self, form, form_framework, form_language):
"""
Called if all forms are valid. Creates a Employee instance along with
associated models and then redirects to a
success page.
"""
self.object = form.save()
form_framework.instance = self.object
form_framework.save()
form_language.instance = self.object
form_language.save()
return HttpResponseRedirect(reverse_lazy('profile', args=[self.kwargs['pk']]))
def form_invalid(self, form, form_framework, form_language):
"""
Called if a form is invalid. Re-renders the context data with the
data-filled forms and errors.
"""
return self.render_to_response(
self.get_context_data(form=form,
form_framework=form_framework,
form_language=form_language,
))
The problem is that I always get an error message when I submit a form:
Select a valid choice. That choice is not one of the available choices.
The problem is that when I submit a form, I always get an error message in the technology field of the queryset which is displayed first in the template.
That is, if the template
{form_framework}}
{form_language}
a mistake on
queryset = Technology.objects.filter(group__name="Framework"
if
{form_language}
{form_framework}}
a mistake on
queryset = Technology.objects.filter(group__name="Programming language"
If I leave only one form in views.py, everything starts to work.
I've been trying to figure it out for 2 days and I think I'm at a dead end. Need help!

How to remove fields from CreateView depending on the user in Django?

I created a CBV of which I want to remove one or more fields, depending on the user. The idea is a jobsite and if the logged in user is a recruiter, than the employer field should be included, otherwise it should be excluded.
forms.py
class JobCreationForm(forms.ModelForm):
class Meta:
model = Job
# exclude = ['posted', 'provider', 'ext_id']
fields = ('title',
'job_desc',
'agency_name',
'employer',
'contact_name',
)
views.py
class JobCreateView(LoginRequiredMixin, CreateView):
template_name = 'job/job.html'
form_class = JobCreationForm
success_url = '/'
def get_context_data(self, **kwargs):
context = super(JobCreateView, self).get_context_data(**kwargs)
# import the Customers of this Company
self.fields["agency_name"].remove()
recruiter = self.request.user
self.fields["contact_name"].queryset = Profile.objects.filter(user_id = self.request.user)
# if the user is a recruiter, delete the employer field.
if Company.objects.filter(user_id = self.request.user).values('is_recruiter') == False:
pass
# self.fields.remove("employer")
del self.fields["employer"]
return context
The current error is NoneType' object has no attribute '__getitem__'.
My question: how can I remove a field from the form based on logic? I tried these versions:
self.fields["employer"].delete()
self.fields.remove("employer")
del self.fields["employer"]
Any tips?
The correct way to implement this (modify the fields of the form depending on user) is to do it on your form's __init__ method. However in order for the form to access the current user you need to pass the user to it from your view. To do this you'll use the get_form_kwargs method. Thus, start by adding the following method to your view:
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs
And now, you can add an __init__ to your form like this:
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if Company.objects.filter(user_id = self.user).is_recruiter == False:
self.fields.pop("employer")
self.fields.pop('owned_by')
Notice that you first initialize the form (using super.__init__) and then you can modify the fields to your heart's content.
There are few ways to go about it.
I find having 2 separate forms RecruiterEmployeeForm and EmployeeForm may be neater.
class RecruiterEmployeeForm(forms.ModelForm):
model = Job
fields = ('title',
'job_desc',
'agency_name',
'employer',
'contact_name',
)
class EmployeeForm(forms.ModelForm):
model = Job
fields = ('title',
'job_desc',
'agency_name',
'contact_name',
)
Then you can override ger_form_class for the CBV
def get_form_class(self):
if self.request.user.is_recruiter():
return RecruiterEmployeeForm
else:
return EmployeeForm
To send extra kwargs to use generic view method get_form_kwargs and to get extra kwargs override __init__ of form and pop the extra kwargs.
forms.py
class JobCreationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super(JobCreationForm, self).__init__(*args, **kwargs)
if Company.objects.filter(user_id = self.user).is_recruiter == False:
self.fields.pop("employer")
class Meta:
model = Job
# exclude = ['posted', 'provider', 'ext_id']
fields = ('title', 'job_desc', 'agency_name', 'employer', 'contact_name')
views.py
class JobCreateView(LoginRequiredMixin, CreateView):
template_name = 'job/job.html'
form_class = JobCreationForm
success_url = '/'
def get_form_kwargs(self):
kwargs = super(JobCreateView, self).get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs

django - admin model assign value to limit_choices_to from another field inside the model

I have extended the admin model, so for each staff member i can assign other customers only if they are in the same group.
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
is_manager = models.BooleanField(default=False)
group_account = models.CharField(max_length=3,blank=True,null=True)
clients_assigned = models.ManyToManyField(User, limit_choices_to = Q(groups__groupaccount__group_account=group_account),blank=True,null=True,related_name='+')
class UserProfileInline(admin.StackedInline):
model = UserProfile
verbose_name = "User extra"
verbose_name_plural = "extra"
filter_horizontal = ('clients_assigned', )
def save_model(self, request, obj, form, change):
return super().save_model(request, obj, form, change)
class UserAdmin(BaseUserAdmin):
inlines = [UserProfileInline, ]
def get_form(self, request, obj=None, **kwargs):
#permissions reduce for regular staff
if (not request.user.is_superuser):
self.exclude = ("app")
self.exclude = ("user_permissions")
## Dynamically overriding
#self.fieldsets[2][1]["fields"] = ('is_active', 'is_staff','is_superuser','groups')
self.fieldsets[2][1]["fields"] = ('is_active', 'is_staff')
form = super(UserAdmin,self).get_form(request, obj, **kwargs)
return form
and extended the group admin model
class GroupAccount(models.Model):
group_account = models.CharField(,max_length=3,blank=True,null=True)
group = models.OneToOneField(Group,blank=True,null=True)
def save(self, *args, **kwargs):
super(GroupAccount, self).save(*args, **kwargs)
what im trying to do is simply to limit the client list for each manager-user by his and their group indicator(group_account field), means the available client list are those whom have the same specific group as himself, such as '555'
when the group_account = '555' in the DB the result of groups__groupaccount__group_account=group_account are empty
but if i change it Hardcoded to: groups__groupaccount__group_account='555'
its return the relevant result.
is that possible and/or what the alternative?
django 1.9
thanks for the help
You should custom the formfield_for_foreignkey method of StackInline
class UserProfileInline(admin.StackedInline):
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
field = super(UserProfileInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == 'clients_assigned':
u = request.user
if not u.is_superuser:
field.queryset = field.queryset.filter(groups__in=u.groups.all())
return field

Django ModelForm - Performance issue

I am using ModelForm to allow multiple rows edit at the same time. It is a very simple form that has series of yes_no columns. The model looks like:
models.py
class Yn(models.Model):
yn_id = models.IntegerField(primary_key=True)
description = models.CharField(max_length=30)
def __str__(self):
return ' '.join([
self.description,
])
class Meta:
managed = False
db_table = 'yn'
class Invoice(models.Model):
description = models.CharField(max_length=50)
invoice_date = models.DateTimeField()
...
invoice_sent_yn = models.ForeignKey('Yn', models.DO_NOTHING, db_column='invoice_sent_yn', related_name="invoice_sent_yn")
confirm_receipt_yn = models.ForeignKey('Yn', models.DO_NOTHING, db_column='confirm_receipt_yn', related_name="confirm_receipt_yn")
paid_yn = models.ForeignKey('Yn', models.DO_NOTHING, db_column='paid_yn', related_name="paid_yn")
forms.py
class InvoiceGridEdit(ModelForm):
model = Invoice
fields = ['description','invoice_date','invoice_sent_yn', 'confirm_receipt_yn', 'paid_yn']
def __init__(self, *args, **kwargs):
super(JurisGridEditForm, self).__init__(*args, **kwargs)
...
...
InvoiceFormSet = modelformset_factory(models.Invoice, form=InvoiceGridEdit)
views.py
class InvoiceUpdateGrid(CreateView):
template_name = "/invoice_update_grid.html"
model = Invoice
form_class = InvoviceViewEditForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect("account_login")
def get(self, request, *args, *kwargs):
self.object = None
customer_number = self.request.GET.get('customer_number')
invoice_form = InvoiceFormSet(queryset=Invoice.objects.filter(customer_number = customer_number)
return self.render_to_response(self.get_context_data(invoice_form=invoice_form))
def post(self, request, *args, **kwargs):
self.object = None
invoice_form = InvoiceFormSet(self.request.POST)
if (invoice_form.is_valid()):
return self.form_valid(invoice_form)
else:
return self.form_invalid(invoice_form)
def form_valid(self, invoice_form):
...
...
invoice_form.save()
return redirect("customer_list")
def form_invalid(self, invoice_form):
return self.render_to_response(self.get_context_data(invoice_form=invoice_form))
The forms works fine, get and post works, except, it takes a while (~30 sec) to retrieve and update. Using django-debug-toolbar, it looks like the yn columns retrieve separately for each column for each row (~2k rows). The Yn table only has 3 rows -1 - Unknown, 0 - No, 1 - Yes.
I tried to search for work around to stop the craziness of Django hitting the DB 900 times per retrieval. I found something about caching but I have no idea how to do it.
Thanks in advance.

filter foreignkey field in django admin

I have these models:
class Entity(models.Model):
name=models.CharField(max_length=100)
class Theme(models.Model):
name=models.CharField(max_length=100)
entity=models.OneToOneField(Entity)
class Company(models.Model):
name=models.CharField(max_length=100)
theme=models.OneToOneField(Theme,null=True,blank=True)
I want to filter the theme field when adding a Company in the admin, something like this:
class CompanyAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(CompanyAdmin, self).queryset(request)
qs.theme.queryset = Theme.objects.filter(name__iexact='company')
return qs
admin.site.register(Company, CompanyAdmin)
I've tried many things, but nothing worked! How can I do this?
Use the render_change_form method:
class CompanyAdmin(admin.ModelAdmin):
def render_change_form(self, request, context, *args, **kwargs):
context['adminform'].form.fields['theme'].queryset = Theme.objects.filter(name__iexact='company')
return super(CompanyAdmin, self).render_change_form(request, context, *args, **kwargs)
I actually prefer to do it in get_form like so:
Django < 2:
class CompanyAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(CompanyAdmin, self).get_form(request, obj, **kwargs)
form.fields['theme'].queryset = Theme.objects.filter(name__iexact='company')
return form
Django >= 2
class CompanyAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(CompanyAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['theme'].queryset = Theme.objects.filter(name__iexact='company')
return form
look here http://books.agiliq.com/projects/django-admin-cookbook/en/latest/filter_fk_dropdown.html
#admin.register(Hero)
class HeroAdmin(admin.ModelAdmin, ExportCsvMixin):
...
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "category":
kwargs["queryset"] = Category.objects.filter(name__in=['God', 'Demi God'])
return super().formfield_for_foreignkey(db_field, request, **kwargs)
In Django 3 it is easy :
class CompanyAdmin(admin.ModelAdmin):
list_display = ('name','theme')
list_filter = ('theme__name',)
admin.site.register(Company,CompanyAdmin)
This will show you a filter on the right of your screen with the list of your theme's name.
Another option is to create a custom model form where the queryset attribute of the theme field will be fine tuned to meet your needs.
class CompanyForm(ModelForm):
class Meta:
model = CompanyForm
fields = __all__ # or a tuple of fields
def __init__(self, *args, **kwargs):
super(CompanyForm, self).__init__(*args, **kwargs)
if self.instance: # Editing and existing instance
self.fields['theme'].queryset = Theme.objects.filter(name__iexact='company')
This model form can be also reused outside of the django admin area.
I faced with the need to add filter to the foreignKey queryset of the parent ModelAdmin class (which all other ModelAdmins inherit from), that is, I can’t know exactly which model I need, this is my solution: db_field.related_model.objects.filter()
class TSModelAdmin(admin.ModelAdmin):
exclude = ('site',)
...
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.related_model:
kwargs["queryset"] =
db_field.related_model.objects.filter(site=request.user.user_profile.site)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
used django version 2.2.10
A bit unrelated, but similar to this so I'll post this here.
I was looking for a way to remove the NULL choice selection on a ModelForm foreignkey field. I first thought I could filter the queryset as is done in other answers here, but that didn't work.
I found that I can filter the entry where the pk value is NULL like this in the get_form method:
class CompanyAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
# remove null choice
form.base_fields["theme"].choices = ((pk, display) for pk, display in form.base_fields["theme"].choices if pk)
return form

Categories