Model Admin Search - Override the search string - django

I have a phone number field in a ModelForm which users can search for in admin. The problem is that they are lazy and don't want to enter in the dashes in the phone numbers.
If I search for '555-555-5555' all the objects with that phone number will return
If I search '5555555555', I get zero results.
Is there anyway to override or just alter the search string that gets submitted? If so I planned on doing something like
if search_string.isdigit() and len(search_string) == 10:
search_string = '-'.join(
(search_string[:3],search_string[3:6],search_string[6:])
)
I see in Django 1.6 there is a get_search_results method that might be useful but I'm running on 1.4

Was able to achieve this by overriding get_changelist within my ModelAdmin. Found a useful blog post that led me to the answer: Override ModelAdmin ChangeList
def get_changelist(self, request, **kwargs):
# Allow users to not have to enter in '-' when searching by phone #
from django.contrib.admin.views.main import ChangeList
class NewChangeList(ChangeList):
def get_query_set(self, *args, **kwargs):
query = self.query
if query.isdigit() and len(query) == 10:
self.query = '-'.join((query[:3], query[3:6], query[6:]))
return super(NewChangeList, self).get_query_set(*args, **kwargs)
return NewChangeList

Related

Django conditional field display on form

I am trying to make a simple form, that conditionally shows the website input field based on the value of another database field (that is not on the form) status. For the sake of this process the status field is not editable by the user, just by the admin. Both fields are in the same table: profile.
After working at this for a while I copped-out and just did the conditional hiding and showing on the template. But, I realise this is the unsophisticated method, and would like to improve it.
What I tried so far in forms.py:
class WebsiteForm(forms.ModelForm):
class Meta:
model = Profile
fields = (
'e-mail',
'website',
)
if Profile.status == 'personal' :
exclude = ('website',)
This method in forms.py works effectively, in that I can conditionally show and hide the field if I use test comparitors in the if statement like:
if 1 == 1:
or
if 1 != 1:
But, I cannot get an effective test using the field Profile.status, the value in the field seems to be unavailable at the point the if test in forms.py is performed.
If I use print(Profile.status) I get the following output in the terminal: user__profile__status__isnull, so I think this means that I am at least testing the correct field in the database. Although I am also noting that this output only shows at initialisation of runserver, not when the form page is accessed.
One final point, the user is authenticated and editing their own record.
Any help very much appreciated.
After a lot of trial and even more error, and some wide-ranging searching, I found the answer via the documentation at https://ccbv.co.uk/.
Essentially the path I decided to take was to use a different form for the respective fields that I wanted to use (I'm sure there are other solutions out there that add or subtract fields from the views). This involved changing the form_class with get_form_class:
# views.py
class telephone_view(UpdateView):
template_name = 'account/telephone.html'
#no need to define "form_class" here
#form_class = TelephoneForm
success_url = '/accounts/telephone/'
def get_form_class(self):
if self.request.user.profile.status == 'managed':
messages.success(self.request, _('you got the managed form'))
return TelephoneFormExtended
else:
messages.success(self.request, _('you got the other form'))
return TelephoneFormLight
def get_object(self, queryset=None):
return Profile.get_or_create_for_user(self.request.user)
def form_valid(self, form):
messages.success(self.request, _('Your telephone setting was updated'))
return super(telephone_view, self).form_valid(form)
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(telephone_view, self).dispatch(*args, **kwargs)
After working it out for myself I also found this answer which does the same thing:
Updateview with dynamic form_class

Django - Admin forms data generation with fields which are not in model

I have a specific scenario and I am unable to figure out how to approach this problem, some direction will be great help:
I have a model:
class RollNumber(models.Model):
r_no_prefix = models.CharField(max_length=10, verbose_name='Roll number
suffix')
r_no= models.IntegerField(verbose_name='Number')
r_no_suffix = models.CharField(max_length=10, verbose_name='Roll number
prefix')
def __unicode__(self):
return '%s -%s-%s' % (self.r_no_prefix,self.r_no,self.r_no_suffix)
No, I want to generate these Roll numbers in bulk by asking the user to input the following in a form which is not having any of the above model fields.
Number of roll numbers you want to generate: ____________
Roll number prefix: ________________
Roll number suffix: ________________
[SUBMIT][CANCEL]
The submission of above form should be able to generate the number of rollnumbers and create records in RollNumber table in bulk.
If I try to use this form again, if should get the last number and then start the sequence from there. Considering the that user may have deleted some of the roll number records.
Don't use a model form, use a simple form and create the objects in a loop. Something like this:
from django import forms
from models import RollNumber
class RollForm(forms.Form):
times_to_roll = forms.IntegerField()
prefix = forms.IntegerField()
suffix = forms.IntegerField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
rolls = RollNumber.objects.all()
if not rolls.exists():
return
last_roll = rolls.order_by('pk')[-1]
self.fields['prefix'].initial = last_roll.prefix
self.fields['suffix'].initial = last_roll.suffix
def is_valid(self, *args, **kwargs):
if not super(RollForm, self).is_valid(*args, **kwargs):
return False
for x in range(self.cleaned_data.times_to_roll):
RollNumber.objects.create(...)
return True

Django: limit models.ForeignKey results

I have an order model:
class Order(models.Model):
profile = models.ForeignKey(Profile, null=True, blank=True)
Which returns all possible profiles for an order, which is not necessary and slowing down the loading of the admin order page.
I want the profile returned to simply be the profile of the user who placed the order. I've tried changing it to:
class Order(models.Model):
profile = models.ForeignKey(Profile, null=True, blank=True, limit_choices_to={'order': 99999})
which returns the correct profile for order number 99999, but how can I get this dynamically. The Order model is not aware of the 'self', but the order number is contained in the URL.
What is the best way to do this?
If you are using the Django Admin, you can override the method formfield_for_foreignkey on your ModelAdmin class to modify the queryset for the profile field dinamically based on a GET parameter for instance (as you have access to request inside the method.
Look at this example from the docs (adapted for your case):
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "profile":
# You can get the order number from the GET parameter:
# order = request.GET.get('order')
# or a fixed number:
order = '12345'
kwargs["queryset"] = Profile.objects.filter(order=order)
return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
Reference in the docs: https://docs.djangoproject.com/en/1.9/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey
I took another look at this and it seems to be working, although it seems like a bit of hack! The problem was that the order number doesn't seem to exist in the request so I am parsing the URL requested. I put this in my order admin:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "profile":
try:
order = int(filter(str.isdigit, str(request.path_info)))
except:
order = request.GET.get('order')
kwargs["queryset"] = Profile.objects.filter(order=order)
return super(OrderAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
The line that filters the url string for an integer works for the change order page of the admin, but didn't work for the order page overview, so I added the try/except. Any suggestions/improvements welcome!
I assume from the context you're referring to the display on the Django Admin page. If you set
raw_id_fields = {'my_foreign_key'}
you will get a number, text description of related model (from str) and a nice popup box to open an instance of the admin page for your related models.
You could alternatively use
list_select_related = True
to get the same behaivour you have now but with a couple of order of magnitudes lower number of queries.

Validate Django model data by calculating the sum of multiple rows

I have two models:
class User(models.Model):
name = models.CharField(max_length=32)
class Referral(models.Model):
referring_user = models.ForeignKey(User, related_name="referrals")
referred_user = models.ForeignKey(User, related_name="referrers")
percentage = models.PositiveIntegerField()
The idea is that every user has n referrers, and should have at least one. Each referrer has a percentage value which should add up to 100% when added to the other referrers.
So User "Alice" might have referrers "Bob" (50%) and "Cynthia" (50%), and User "Donald" might have one referrer: "Erin" (100%).
The problem I have is with validation. Is there a way (preferably one that plays nice with the Django admin using admin.TabularInline) that I can have validation reject the saving of a User if the sum of Refferrals != 100%?
Ideally I want this to happen at the form/admin level and not by overriding User.save(), but at this point I don't know where to start. Most of Django's validation code appears to be atomic, and validation across multiple rows is not something I've done in Django before.
After Jerry Meng suggested I look into the data property and not cleaned_data, I started poking around admin.ModelAdmin to see how I might access that method. I found get_form which appears to return a form class, so I overrode that method to capture the returning class, subclass it, and override .clean() in there.
Once inside, I looped over self.data, using a regex to find the relevant fields and then literally did the math.
import re
from django import forms
from django.contrib import admin
class UserAdmin(admin.ModelAdmin):
# ...
def get_form(self, request, obj=None, **kwargs):
parent = admin.ModelAdmin.get_form(self, request, obj=None, **kwargs)
class PercentageSummingForm(parent):
def clean(self):
cleaned_data = parent.clean(self)
total_percentage = 0
regex = re.compile(r"^referrers-(\d+)-percentage$")
for k, v in self.data.items():
match = re.match(regex, k)
if match:
try:
total_percentage += int(v)
except ValueError:
raise forms.ValidationError(
"Percentage values must be integers"
)
if not total_percentage == 100:
raise forms.ValidationError(
"Percentage values must add up to 100"
)
return cleaned_data
return PercentageSummingForm
As per the Django docs, clean() is the official function to implement for your purposes. You could imagine a function that looks like this:
from django.core.exceptions import ValidationError
def clean(self):
total_percentage = 0
for referrer in self.referrers.all():
total_percentage += referrer.percentage
if total_percentage !== 100:
raise ValidationError("Referrer percentage does not equal 100")

Concatenate value of two models by overriding ModelForm and ModelChoiceField

To show just the just the related Projects in a ForeignKey Selectbox in Django AdminForm, i customized my ActionAdmin Model with a ActionAdminForm class. to preselect values i used a class like posted here https://stackoverflow.com/a/9191583/326905. Thanks a lot, this works really fine.
But when user does not navigate form Customer -> Project -> Action and navigates directly to Actions in django admin i want to display the values in the selectbox for foreignkey project in ActionAdmin Form formatted like this:
Customername1 - Projectname1
Customername1 - Projectname2
Customername2 - Projectname3
My question is, how could i override self.fields["project"]
in the else case in the code below, so that i get selectbox values concatenated from Project.customer.name and Project.name?
class ActionAdminForm(ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(ActionAdminForm, self).__init__(*args, **kwargs)
if self.request.GET.get('project'):
prj = Project.objects.get(id=self.request.GET.get('project'))
self.fields["project"].queryset = Project.objects.filter(customer = prj.customer)
else:
self.fields["project"] = ProjectModelChoiceField(Project.objects.all().order_by('name'))
class Meta:
model = Action
I got the solution. Yeah. First i got always error when i tried to use just self.fields["project"], but now it works. I put it into else and wrote a ProjectModelChoiceField like below, influenced by this description: http://bradmontgomery.blogspot.de/2009/01/custom-form-for-djangos-automatic-admin.html
class ProjectModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return "%s - %s"%(obj.customer.name, obj.name)