Django FilePathField filter used entries - python

I have the following Model where the FilePathField should be unique:
class Gallery(models.Model):
template = models.FilePathField(path=".../templates/galleries/", unique=True)
In the admin, I would like the dropdown list to only show me those entries that have not been used, yet, in order to make the selection among available answers more easy.
After all, any already used option in the resulting dropdown list will give me an error anyway and does not need to be shown to me in the admin. Unfortunately I am having problems wrapping my head around this.
Can anyone tell me where I could insert something similar to the following:
used = [gallery.template for gallery in Gallery.objects.all()]
return [file for file in files if file not in used]
...or might I have overseen an option somewhere in Django that could already give me the desired result? Any help would be appreciated.

So, after a lot of digging, I managed to come up with a solution myself. Ill post it here as an answer if anyone seeks a similar solution:
Extend a ModelAdmin for your Model and implement a new get_form() method that takes the choices of your named FilePathField and filter this list to your liking.
Ill give an example for the Gallery Model above:
class GalleryAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
used = [gallery.template for gallery in Gallery.objects.all()]
form = super(GalleryAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['template'].choices = [choice for choice in form.base_fields['template'].choices if choice[0] not in used]
return form
EDIT: I noticed this prevents you from changing an entry, as the option originally set will now be removed. I managed to get this to work with this small tweak:
class GalleryAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if obj: # we are changing an entry
used = [gallery.template for gallery in Gallery.objects.all() if gallery.template != obj.template]
else: # we are adding a new entry
used = [gallery.template for gallery in Gallery.objects.all()]
form = super(GalleryAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['template'].choices = [choice for choice in form.base_fields['template'].choices if choice[0] not in used]
return form
Hope this may help anyone in the future!

Related

Indirect way of usine Model.objects.all() in a formset

I'm using something like this to populate inlineformsets for an update view:
formset = inline_formsetfactory(Client, Groupe_esc, form=GroupEscForm, formset=BaseGroupEscInlineFormset, extra=len(Groupe.objects.all()))
(Basically I need as many extra form as there are entries in that table, for some special processing I'm doing in class BaseGroupEscInlineFormset(BaseInlineFormset)).
That all works fine, BUT if I pull my code & try to makemigrations in order to establish a brand new DB, that line apparently fails some django checks and throws up a "no such table (Groupe)" error and I cannot makemigrations. Commenting that line solves the issues (then I can uncomment it after making migration). But that's exactly best programming practices.
So I would need a way to achieve the same result (determine the extra number of forms based on the content of Groupe table)... but without triggering that django check that fails. I'm unsure if the answer is django-ic or pythonic.
E.g. perhaps I could so some python hack that allows me to specific the classname without actually importing Groupe, so I can do my_hacky_groupe_import.Objects.all(), and maybe that wouldn't trigger the error?
EDIT:
In forms.py:
from .models import Client, Groupe
class BaseGroupEscInlineFormset(BaseInlineFormSet):
def get_form_kwargs(self, index):
""" this BaseInlineFormset method returns kwargs provided to the form.
in this case the kwargs are provided to the GroupEsForm constructor
"""
kwargs = super().get_form_kwargs(index)
try:
group_details = kwargs['group_details'][index]
except Exception as ex: # likely this is a POST, but the data is already in the form
group_details = []
return {'group_details':group_details}
GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
extra=len(Groupe.objects.all()),
can_delete=False)
The issue as already outlined is that your code is written at the module level and it executes a query when the migrations are not yet done, giving you an error.
One solution as I already pointed in the comment would be to write the line to create the formset class in a view, example:
def some_view(request):
GroupeEscFormset = inlineformset_factory(
Client,
Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
extra=len(Groupe.objects.all()),
can_delete=False
)
Or if you want some optimization and want to keep this line at the module level to not keep recreating this formset class, you can override the __init__ method and accept extra as an argument (basically your indirect way to call Model.objects.all()):
class BaseGroupEscInlineFormset(BaseInlineFormSet):
def __init__(self, *args, extra=3, **kwargs):
self.extra = extra
super().__init__(*args, **kwargs)
...
GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
can_delete=False)
# In your views:
def some_view(request):
formset = GroupeEscFormset(..., extra=Groupe.objects.count()) # count is better if the queryset is needed only to get a count

How to use variables in ModelChoiceField queryset in Django

I am trying to use a specific list of data in my form with Django. I am using ModelChoiceField to retrieve from the model the data I need to display in the form (to let the users select from a scolldown menu).
My query is complicate because need two filters based on variables passed by views
I've tried to use the sessions but in form is not possible to import the session (based to my knowledge).
form.py
def __init__(self, pass_variables, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['initiative'] = forms.ModelChoiceField(queryset=raid_User_Initiative.objects.filter(company=pass_variables[1], username=pass_variables[0]).values_list('initiative', flat=True))
view.py
pass_variables = ((str(request.user), companyname))
f = Issue_form(pass_variables)
If I don't pass the variable the process works. The problem is with the code above as the form don't provide any error but it doesn't pass the if f.is_valid():
Thanks
I solved myself! Anyway if anyone interested the solution is using the sessions:
form.py
Before I declare the queryset as:
initiative = forms.ModelChoiceField(queryset=raid_User_Initiative.objects.all(), to_field_name="initiative")
And it is very important to use the , to_field_name="initiative"
After I amend the queryset as:
def __init__(self, user, company, *args, **kwargs):
super(raid_Issue_form, self).__init__(*args, **kwargs)
self.fields['initiative'].queryset = raid_User_Initiative.objects.filter(company=company, username=user).values_list('initiative', flat=True)
view.py
f = raid_Issue_form(request.user, request.session['company'])
Hope this help!

Primary keys as choices in MultipleChoiceField

The thing is quite obvious to my mind, still I can get it working.
Previously I tried to get the filtered model instances from MultipleModelChoiceField by overriding the __init__ method and it worked as expected. Now I need to get only pk from those instances and I decided to do it in MultipleChoiceField. I try to do it the following way but do not succeed:
class AnswerForm(forms.Form):
answers = forms.MultipleChoiceField(
choices = [answer.pk for answer in Answer.objects.all()],
widget = forms.CheckboxSelectMultiple,
)
def __init__(self, *args, **kwargs):
q_pk = kwargs.pop('q_pk')
super(AnswerForm, self).__init__(*args, **kwargs)
self.fields['answers'].choices = [answer.pk for answer in Answer.objects.filter(question__pk=q_pk)]
In a nutshell: don't do this, stick with ModelMultipleChoiceField.
It obviously won't work because choices expects a list of tuples. Taking that in account, [answer.pk for answer in Answer.objects.filter(question__pk=q_pk)] can be rewritten like Answer.objects.filter(question__pk=q_pk).values_list('pk', 'someotherfield'), which brings you back to what ModelMultipleChoiceField does.
Many thanks to Ivan for his pointing me at using ModelChoiceField.
It is my inattention, since I only now figured out that I need some other model fields (except pk) to be passed to the form as well.
In that case the best way, that I found to get the model primary key as a value of a chosen input(s) is to get the entire models from form first and then iterate them to get the desired field value as follows:
forms.py
class AnswerForm(forms.Form):
answer = forms.ModelMultipleChoiceField(
queryset = Answer.objects.all(),
widget = forms.CheckboxSelectMultiple,
)
def __init__(self, *args, **kwargs):
q_pk = kwargs.pop('q_pk', None)
super(AnswerForm, self).__init__(*args, **kwargs)
self.fields['answer'].queryset = Answer.objects.filter(question__pk=q_pk)
views.py
checked = [answer.pk for answer in form.cleaned_data['answer']]

Remove "add another" in Django admin screen

Whenever I'm editing object A with a foreign key to object B, a plus option "add another" is available next to the choices of object B. How do I remove that option?
I configured a user without rights to add object B. The plus sign is still available, but when I click on it, it says "Permission denied". It's ugly.
I'm using Django 1.0.2
The following answer was my original answer but it is wrong and does not answer OP's question:
Simpler solution, no CSS hack and no editing Django codebase:
Add this to your Inline class:
max_num=0
(this is only applicable to inline forms, not foreign key fields as OP asked)
The above answer is only useful to hide the "add related" button for inline forms, and not foreign keys as requested.
When I wrote the answer, IIRC the accepted answer hid both, which is why I got confused.
The following seems to provide a solution (though hiding using CSS seems the most feasible thing to do, especially if the "add another" buttons of FKs are in inline forms):
Django 1.7 removing Add button from inline form
Though most of the solutions mentioned here work, there is another cleaner way of doing it. Probably it was introduced in a later version of Django, after the other solutions were presented. (I'm presently using Django 1.7)
To remove the "Add another" option,
class ... #(Your inline class)
def has_add_permission(self, request):
return False
Similarly if you want to disable "Delete?" option, add the following method in Inline class.
def has_delete_permission(self, request, obj=None):
return False
N.B. Works for DJango 1.5.2 and possibly older. The can_add_related property appeared around 2 years ago.
The best way I've found is to override your ModelAdmin's get_form function. In my case I wanted to force the author of a post to be the currently logged in user. Code below with copious comments. The really important bit is the setting of widget.can_add_related:
def get_form(self,request, obj=None, **kwargs):
# get base form object
form = super(BlogPostAdmin,self).get_form(request, obj, **kwargs)
# get the foreign key field I want to restrict
author = form.base_fields["author"]
# remove the green + by setting can_add_related to False on the widget
author.widget.can_add_related = False
# restrict queryset for field to just the current user
author.queryset = User.objects.filter(pk=request.user.pk)
# set the initial value of the field to current user. Redundant as there will
# only be one option anyway.
author.initial = request.user.pk
# set the field's empty_label to None to remove the "------" null
# field from the select.
author.empty_label = None
# return our now modified form.
return form
The interesting part of making the changes here in get_form is that author.widget is an instance of django.contrib.admin.widgets.RelatedFieldWidgetWrapper where as if you try and make changes in one of the formfield_for_xxxxx functions, the widget is an instance of the actual form widget, in this typical ForeignKey case it's a django.forms.widgets.Select.
I use the following approaches for Form and InlineForm
Django 2.0, Python 3+
Form
class MyModelAdmin(admin.ModelAdmin):
#...
def get_form(self,request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
user = form.base_fields["user"]
user.widget.can_add_related = False
user.widget.can_delete_related = False
user.widget.can_change_related = False
return form
Inline Form
class MyModelInline(admin.TabularInline):
#...
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj, **kwargs)
user = formset.form.base_fields['user']
user.widget.can_add_related = False
user.widget.can_delete_related = False
user.widget.can_change_related = False
return formset
The answer by #Slipstream shows how to implement the solution, viz. by overriding the attributes for the formfield's widget, but, in my opinion, get_form is not the most logical place to do this.
The answer by #cethegeek shows where to implement the solution, viz. in an extension of formfield_for_dbfield, but does not provide an explicit example.
Why use formfield_for_dbfield? Its docstring suggests that it is the designated hook for messing with form fields:
Hook for specifying the form Field instance for a given database Field instance.
It also allows for (slightly) cleaner and clearer code, and, as a bonus, we can easily set additional form Field attributes, such as initial value and/or disabled (example here), by adding them to the kwargs (before calling super).
So, combining the two answers (assuming the OP's models are ModelA and ModelB, and the ForeignKey model field is named b):
class ModelAAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, request, **kwargs):
# optionally set Field attributes here, by adding them to kwargs
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
if db_field.name == 'b':
formfield.widget.can_add_related = False
formfield.widget.can_change_related = False
formfield.widget.can_delete_related = False
return formfield
# Don't forget to register...
admin.site.register(ModelA, ModelAAdmin)
NOTE: If the ForeignKey model field has on_delete=models.CASCADE, the can_delete_related attribute is False by default, as can be seen in the source for RelatedFieldWidgetWrapper.
Look at django.contrib.admin.options.py and check out the BaseModelAdmin class, formfield_for_dbfield method.
You will see this:
# For non-raw_id fields, wrap the widget with a wrapper that adds
# extra HTML -- the "add other" interface -- to the end of the
# rendered output. formfield can be None if it came from a
# OneToOneField with parent_link=True or a M2M intermediary.
if formfield and db_field.name not in self.raw_id_fields:
formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
I think your best bet is create subclass of ModelAdmin (which in turn is a subclass of BaseModelAdmin), base your model on that new class, override formfield_fo_dbfield and make it so that it won't/or will conditionally wrap the widget in RelatedFieldWidgetWrapper.
One could argue that if you have a user that doesn't have rights to adding related objects, the RelatedFieldWidgetWrapper should not display the add link? Maybe this is something that is deserving of mention in Django trac?
DEPRECATED ANSWER
Django has since made this possible.
Have you considered instead, using CSS to simply not show the button? Maybe that's a little too hacky.
This is untested, but I'm thinking...
no-addanother-button.css
#_addanother { display: none }
admin.py
class YourAdmin(admin.ModelAdmin):
# ...
class Media:
# edit this path to wherever
css = { 'all' : ('css/no-addanother-button.css',) }
Django Doc for doing this -- Media as a static definition
Note/Edit: The documentation says the files will be prepended with the MEDIA_URL but in my experimentation it isn't. Your mileage may vary.
If you find this is the case for you, there's a quick fix for this...
class YourAdmin(admin.ModelAdmin):
# ...
class Media:
from django.conf import settings
media_url = getattr(settings, 'MEDIA_URL', '/media/')
# edit this path to wherever
css = { 'all' : (media_url+'css/no-addanother-button.css',) }
I'm using Django 2.x and I think I found best solution, at least for my case.
The HTML file to the "Save and Add Another" button is on your_python_installation\Lib\site-packages\django\contrib\admin\templates\admin\subtmit_line.html.
Copy that html file and paste to your project like so your_project\templates\admin\submit_line.html.
Open it and comment/delete the button code as desired:
{#{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}#}
I know this problem is already answered. But maybe someone in the future have a similar case with me.
Based on cethegeek answer I made this:
class SomeAdmin(admin.ModelAdmin):
form = SomeForm
def formfield_for_dbfield(self, db_field, **kwargs):
formfield = super(SomeAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'some_m2m_field':
request = kwargs.pop("request", None)
formfield = self.formfield_for_manytomany(db_field, request, **kwargs) # for foreignkey: .formfield_for_foreignkey
wrapper_kwargs = {'can_add_related': False, 'can_change_related': False, 'can_delete_related': False}
formfield.widget = admin.widgets.RelatedFieldWidgetWrapper(
formfield.widget, db_field.remote_field, self.admin_site, **wrapper_kwargs
)
return formfield
The way i fixed a similar situation based on django docs
https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#django.contrib.admin.InlineModelAdmin.extra
The outcome of the solution is that it lets you add an inline just for that instance. Or in different words: add an inline and just one; no other buttons.
#models.py
class Model_A(models.Model):
...
class Model_B(models.Model):
...
relevant_field = models.ForeignKey(Model_A, related_name='Model_B_relevant_field')
# forms.py or someotherfile.py
from django.contrib.admin import StackedInline, TabularInline
class Model_B_Inline(StackedInline):
verbose_name = 'Some Name'
...
def get_extra(self, request, obj=None, *args, **kwargs):
the_extra = super().get_extra(request, obj=obj, *args, **kwargs)
self.extra = 1
if obj:
the_counter = obj.Model_B_relevant_field.count()
else:
the_counter = -1
self.max_num = the_counter + 1
return the_extra
As it's been pointed out in comments:
max_num = 0
It has also been confirmed here:
https://code.djangoproject.com/ticket/13424#comment:1
PS: This also works for inlines.
django.contrib.admin.widgets.py
(Django Install Dir)/django/contrib/admin/widgets.py: Comment everything between Line 239 & Line 244:
if rel_to in self.admin_site._registry: # If the related object has an admin interface:
# TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically.
output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
(related_url, name))
output.append(u'<img src="%simg/admin/icon_addlink.gif" width="10" height="10" alt="%s"/></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Add Another')))

Accessing MultipleChoiceField choices values

How do I get the choices field values and not the key from the form?
I have a form where I let the user select some user's emails for a company.
For example I have a form like this (this reason for model form is that it's inside a formset - but that is not important for now):
class Contacts(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(Contacts, self).__init__(*args, **kwargs)
self.company = kwargs['initial']['company']
self.fields['emails'].choices = self.company.emails
# This produces stuff like:
# [(1, 'email#email.com'), ...]
emails = forms.MultipleChoiceField(required=False)
class Meta:
model = Company
and I want to get the list of all selected emails in the view, something like this:
form = ContactsForm(request.POST)
if form.is_valid():
form.cleaned_data['emails'][0] # produces 1 and not email
There is no get_emails_display() kind of method, like in the model for example. Also, a suggestion form.fields['emails'].choices does not work, as it gives ALL the choices, whereas I need something like form.fields['emails'].selected_choices?
Any ideas, or let me know if it's unclear.
Ok, hopefully this is closer to what you wanted.
emails = filter(lambda t: t[0] in form.cleaned_data['emails'], form.fields['emails'].choices)
That should give you the list of selected choices that you want.
It might not be a beautiful solution, but I would imagine that the display names are all still available from form.fields['emails'].choices so you can loop through form.cleaned_data['emails'] and get the choice name from the field's choices.

Categories