Django admin and autocomplete-light - how to override save method - django

I'm trying to include Django-autocomplete-light in my project. Everything works as expected including the creation of new choice by autocomplete. The only problem is my model contains more than one field and I'm using autocomplete only on the 'name' field. When I save my new record django-admin creates new object with the same name in the database instead of updating the record already created by autocomplete. At the end I have two records, one half empty created by autocomplete, and one valid created by django-admin.
models.py
class Montinent(models.Model):
name = models.CharField(max_length=250)
code = models.CharField(max_length=2, unique=True, db_index=True)
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ('-name',)
def __str__(self):
return self.name
views.py
class MontinentAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
#Don't forget to filter out results depending on the visitor !
if not self.request.user.is_authenticated():
return Montinent.objects.none()
qs = Montinent.objects.all()
if self.q:
qs = qs.filter(name__istartswith=self.q)
return qs
urls.py
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^montinent-autocomplete/$', MontinentAutocomplete.as_view(create_field='name'), name='montinent-autocomplete',),
]
admin.py
class MontinentForm(forms.ModelForm):
name = forms.ModelChoiceField(
queryset = Montinent.objects.all(),
widget = autocomplete.ModelSelect2(url='montinent-autocomplete')
)
class Meta:
model = Montinent
fields = ['name', 'slug', 'code']
class MontinentAdmin(admin.ModelAdmin):
form = MontinentForm
admin.site.register(Montinent, MontinentAdmin)
The way autocomplete creates new choice is as follow:
When the user selects that option, the autocomplete script will make a
POST request to the view. It should create the object and return the
pk, so the item will then be added just as if it already had a PK.
In this case it looks like I need to override the default django-admin save method. I tried just to make the 'name' field unique but in this case Django says this name already exist.
My question is how to override the default save method in my models.py so django to use the returned from autocomplete pk and append the missing information to the row instead of creating new object?

I was also struggling to fill more than one field with the autocomplete create choice. I needed to include the user who is creating the new entry.
The method that saves the new entry into the database is the create_object(text) method from autocomplete.Select2QuerySetView. You can read more about this method in the documentation http://django-autocomplete-light.readthedocs.io/en/master/api.html
So, to include a user I just override the method as follow:
def create_object(self, text):
return self.get_queryset().create(**{self.create_field: text, 'user' : self.request.user})
Now you don't need to have partially filled forms using the autocomplete create options. Just fill it with any field you want to.

Related

DJANGO , custom LIST_FILTER

I am using only the django admin , and trying to creating a custom filter, where is to filter the date of another model.
My models
class Avaria(models.Model):
.....
class Pavimentacao(models.Model):
avaria = models.ForeignKey(Avaria, related_name='AvariaObjects',on_delete=models.CASCADE)
date= models.DateField(blank=True,null=True)
AvariaAdmin
class AvariaAdmin(admin.ModelAdmin):
list_filter = ('')
For example, Let's say you have a model and you have to add custom ContentTypeFilter to your model admin then. you can define a class which inherit SimpleListFilter and define lookups and queryset based on your requirement and add this class to list_filter like
list_filter = [ContentTypeFilter]
Refer to docs
Example class definition is like below:
class ContentTypeFilter(admin.SimpleListFilter):
# Human-readable title which will be displayed in the
# right admin sidebar just above the filter options.
title = _('content type')
# Parameter for the filter that will be used in the URL query.
parameter_name = 'type'
def lookups(self, request, model_admin):
"""
Returns a list of tuples. The first element in each
tuple is the coded value for the option that will
appear in the URL query. The second element is the
human-readable name for the option that will appear
in the right sidebar.
"""
models_meta = [
(app.model._meta, app.model.__name__) for app in get_config()
]
return (item for item in models_meta)
def queryset(self, request, queryset):
"""
Returns the filtered queryset based on the value
provided in the query string and retrievable via
`self.value()`.
"""
if not self.value():
return
model = apps.get_model(self.value())
if model:
return queryset.models(model)
You have to add the field you want to filter. In your example if you want to filter on date you put list_filter('date'). Dont forget to register the model admin as seen here

django-autocomplete-light initial data using Select2 widget

I have a form field with autocomplete (using django-autocomplete-light app and Select2 widget), which serve as a search filter and it works as expected.
When I submit the form and search results are listed, I would like to set this form field initial value to previously submitted value - so the user can adjust some of the search parameters instead of setting up all search filters from scratch.
This form field will be used to choose one of the ~10000 values, so I need it to load values on-demand. As the form field is not prepopulated with any values, I have no idea how it would be possible to set initial value.
models.py
class Location(models.Model):
place = models.CharField(max_length=50)
postal_code = models.CharField(max_length=5)
views.py
class LocationAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
qs = Location.objects.all()
if self.q:
qs = qs.filter(place__istartswith=self.q) | qs.filter(postal_code__istartswith=self.q)
return qs
forms.py
class LocationForm(forms.ModelForm):
class Meta:
model = Location
fields = ('place',)
widgets = {
'place': autocomplete.Select2(url='location_autocomplete')
}
Any suggestions?
Thanks!
In your views.py if you pass place value like this:
LocationForm(initial={'place': place })
It will be pre-populated in your form.
Docs: https://docs.djangoproject.com/en/1.11/ref/forms/fields/#initial

How can I create model instance via Serializer without creating models from nested serialziers?

I have model with many links into it:
class Travel(BaseAbstractModel):
tags = models.ManyToManyField(
Tag,
related_name='travels',
)
owner = models.ForeignKey(
'users.TravelUser',
related_name='travel_owner'
)
payment = models.ForeignKey(
Payment,
related_name='travels',
)
country = models.ForeignKey(
Country,
related_name='travels,
)
........
Many of these models have only two fields with unique name and image.
I create serializer for each of these models and put them in TravelSerializer
class TravelBaseSerializer(DynamicFieldsModelSerializer):
owner = UserSerializer(required=False)
tags = TagSerializer(many=True)
payment = PaymentSerializer()
country = CountrySerializer()
Based on docs I override create() and update.
The problem is, when I sent JSON data, Django create each model from nested serializers. But I want to create only Travel instance. Also I want receive and respond serialized object not only pk field.
UPDATE
I solved this problem, put code in the answer. Now I can receive and respond with Serializer data without creating object.
But I think the DRF provides more elegant approach then I do. It is my first project with DRF, maybe I miss something and there's an easier solution.
I decide override to_internal_value() put it in custom serailizer and inherit all nested serializers from it:
class NestedRelatedSerializer(serializers.ModelSerializer):
def to_internal_value(self, data):
try:
pk = data['pk']
except (TypeError, KeyError):
# parse pk from request JSON
raise serializers.ValidationError({'_error': 'object must provide pk!'})
return pk
Get all pk from it and save in create and updated methods:
def update(self, instance, validated_data):
# If don't get instance from db, m2m field won't update immediately
# I don't understand why
instance = Travel.objects.get(pk=instance.pk)
instance.payment_id = validated_data.get('payment', instance.payment_id)
instance.country_id = validated_data.get('country', instance.country_id)
# update m2m links
instance.tags.clear()
instance.tags.add(*validated_data.get('tags'))
instance.save()
return instance
I'm not exactly sure I understand what you want to do, but could setting read_only_fields is the Meta class be what you need ?
class TravelBaseSerializer(DynamicFieldsModelSerializer):
owner = UserSerializer(required=False)
tags = TagSerializer(many=True)
payment = PaymentSerializer()
country = CountrySerializer()
class Meta:
read_only_fields = ('tags',)
See this section in the docs.

Using an instance's fields to filter the choices of a manytomany selection in a Django admin view

I have a Django model with a ManyToManyField.
1) When adding a new instance of this model via admin view, I would like to not see the M2M field at all.
2) When editing an existing instance I would like to be able to select multiple options for the M2M field, but display only a subset of the M2M options, depending on another field in the model. Because of the dependence on another field's actual value, I can't just use formfield_for_manytomany
I can do both of the things using a custom ModelForm, but I can't reliably tell whether that form is being used to edit an existing model instance, or if it's being used to create a new instance. Even MyModel.objects.filter(pk=self.instance.pk).exists() in the custom ModelForm doesn't cut it. How can I accomplish this, or just tell whether the form is being displayed in an "add" or an "edit" context?
EDIT: my relevant code is as follows:
models.py
class LimitedClassForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LimitedClassForm, self).__init__(*args, **kwargs)
if not self.instance._adding:
# Edit form
clas = self.instance
sheets_in_course = Sheet.objects.filter(course__pk=clas.course.pk)
self.Meta.exclude = ['course']
widget = self.fields['active_sheets'].widget
sheet_choices = []
for sheet in sheets_in_course:
sheet_choices.append((sheet.id, sheet.name))
widget.choices = sheet_choices
else:
# Add form
self.Meta.exclude = ['active_sheets']
class Meta:
exclude = []
admin.py
class ClassAdmin(admin.ModelAdmin):
formfield_overrides = {models.ManyToManyField: {
'widget': CheckboxSelectMultiple}, }
form = LimitedClassForm
admin.site.register(Class, ClassAdmin)
models.py
class Course(models.Model):
name = models.CharField(max_length=255)
class Sheet(models.Model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
file = models.FileField(upload_to=getSheetLocation)
class Class(models.model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
active_sheets = models.ManyToManyField(Sheet)
You can see that both Sheets and Classes have course fields. You shouldn't be able to put a sheet into active_sheets if the sheet's course doesn't match the class's course.

Django admin: How to display a field that is marked as editable=False' in the model?

Even though a field is marked as 'editable=False' in the model, I would like the admin page to display it. Currently it hides the field altogether.. How can this be achieved ?
Use Readonly Fields. Like so (for django >= 1.2):
class MyModelAdmin(admin.ModelAdmin):
readonly_fields=('first',)
Update
This solution is useful if you want to keep the field editable in Admin but non-editable everywhere else. If you want to keep the field non-editable throughout then #Till Backhaus' answer is the better option.
Original Answer
One way to do this would be to use a custom ModelForm in admin. This form can override the required field to make it editable. Thereby you retain editable=False everywhere else but Admin. For e.g. (tested with Django 1.2.3)
# models.py
class FooModel(models.Model):
first = models.CharField(max_length = 255, editable = False)
second = models.CharField(max_length = 255)
def __unicode__(self):
return "{0} {1}".format(self.first, self.second)
# admin.py
class CustomFooForm(forms.ModelForm):
first = forms.CharField()
class Meta:
model = FooModel
fields = ('second',)
class FooAdmin(admin.ModelAdmin):
form = CustomFooForm
admin.site.register(FooModel, FooAdmin)
Add the fields you want to display on your admin page.
Then add the fields you want to be read-only.
Your read-only fields must be in fields as well.
class MyModelAdmin(admin.ModelAdmin):
fields = ['title', 'author', 'published_date', 'updated_date', 'created_date']
readonly_fields = ('updated_date', 'created_date')
You could also set the readonly fields as editable=False in the model (django doc reference for editable here). And then in the Admin overriding the get_readonly_fields method.
# models.py
class MyModel(models.Model):
first = models.CharField(max_length=255, editable=False)
# admin.py
class MyModelAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
return [f.name for f in obj._meta.fields if not f.editable]
With the above solution I was able to display hidden fields for several objects but got an exception when trying to add a new object.
So I enhanced it like follows:
class HiddenFieldsAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
try:
return [f.name for f in obj._meta.fields if not f.editable]
except:
# if a new object is to be created the try clause will fail due to missing _meta.fields
return ""
And in the corresponding admin.py file I just had to import the new class and add it whenever registering a new model class
from django.contrib import admin
from .models import Example, HiddenFieldsAdmin
admin.site.register(Example, HiddenFieldsAdmin)
Now I can use it on every class with non-editable fields and so far I saw no unwanted side effects.
You can try this
#admin.register(AgentLinks)
class AgentLinksAdmin(admin.ModelAdmin):
readonly_fields = ('link', )