django manytomany model relationship crashing admin on object create - django

I have an Event object in my postgres db, and created a new Collection object to group events by theme via a ManyToMany field relationship:
class Collection(models.Model):
event = models.ManyToManyField('Event', related_name='collections')
name = models.CharField(blank=True, max_length=280)
slug = AutoSlugField(populate_from='name')
image = models.ImageField(upload_to='collection_images/', blank=True)
description = models.TextField(blank=True, max_length=1000)
theme = models.ManyToManyField('common.Tag', related_name='themes')
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=False)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('collection', args=[self.slug])
def clean(self):
# because of the way db saves M2M relations, collection doesn't have a
# type at this time yet, so image inheritance is
# called from the signal which is triggered when M2M is created
# (that means if an image is later deleted, it won't inherit a new
# one when collection is saved)
if self.image:
validate_hero_image(self.image, 'image')
def save(self, *args, **kwargs):
try:
self.full_clean()
except ValidationError as e:
log.error('Collection validation error (name = %s): %s' % (self.name, e))
return super(Collection, self).save(*args, **kwargs)
in my admin, I'm defining and registering CollectionAdmin like this:
class CollectionAdmin(admin.ModelAdmin):
model = Collection
verbose_name = 'Collection'
list_display = ( 'name', )
however, if I go into admin and attempt to create a Collection "GET /admin/app/collection/add/" 200, the request frequently times out and the query load on my database from the Event M2M relationship seems quite heavy from logging. For reference currently the db has ~100,000 events. are there better ways to (re)structure my admin fields so I can select specific events (by name or id) to add to a Collection without effectively requesting a QuerySet of all events when that view is loaded (or creating them in db via shell)? thanks

There are multiple ways to accomplish this. For example, you could override the form fields the admin uses and specify another widget to use, like NumberInput.
You could also add your event model field to the raw_id_fields attribute of ModelAdmin. By doing so, Django won't try to create a fully populated select input but will offer you a way to search for events manually if needed:
class CollectionAdmin(admin.ModelAdmin):
model = Collection
verbose_name = 'Collection'
list_display = ('name', )
raw_id_fields = ('event', )

Related

Create multiple model instances of interlinked models through one post request in DRF

I want to create two entries from one post request. One in the 'Dates' model and one in 'Other' model. Code corresponding to both models is shown below.
class Dates(models.Model):
booking_id = models.AutoField(primary_key=True)
timestamp = models.DateTimeField(auto_now_add=True)
feedback = models.CharField(max_length=8, default='no')
myself = models.BooleanField(default=True)
class Meta:
app_label = 'bookings'
Other is:
class Other(models.Model):
booking_id = models.OneToOneField(
'bookings.Dates',
null=False,
default=1,
primary_key=True,
on_delete=models.CASCADE
)
name = models.CharField(max_length=64)
phone_number = models.CharField(max_length=14)
email_id = models.EmailField(max_length=128)
class Meta:
app_label = 'bookings'
I have validated the data from Dates Serializer and created the object in 'Dates' table. Now, I want to use the generated 'booking_id' as the same 'booking_id' for 'Other' table. How can I validate serializer and create an object in the 'Other' table while maintaining the consistency?
Here with consistency, I mean: Either create objects in both the tables if no error occurs or don't create an object if any error occurs.
You can make use of writable nested serializers to achieve this. You need to define a serializer class for Other model, then your Dates serializer can look like this:
class DatesSerializer(serializers.ModelSerializer):
other = OtherSerializer()
class Meta:
model = Dates
fields = ('timestamp', 'feedback', 'myself', 'other')
def validate_other(self, value):
# Run validations for Other model here, either manually or through OtherSerializer's is_valid method. You won't have booking_id in value here though, take that into account when modelling your validation process
def validate_feedback(self, value):
# Run validations specific to feedback field here, if necessary. You can do this for all serializer fields
def validate(self, data):
# Run non-field specific validations for Dates here
def create(self, validated_data):
# At this point, validation for both models are run and passed
# Pop other model data from validated_data first
other_data = validated_data.pop('other')
# Create Dates instance
dates = Dates.objects.create(**validated_data)
# Create Other instance now
Other.objects.create(booking_id=dates, **other_data)
return dates
You can use the defaul CreateModelMixin of DRF here, all nested object logic is handled in the serializer.

Django - one-to-one modelAdmin

I am moderately proficient with django and trying to use model forms for an intranet project.
Essentially, I have a "Resources" model, which is populated by another team.
Second model is "Intake", where users submit request for a resource. It has one-to-one mapping to resource.
Objective is to only allow 1 resource allocation per intake.
Now, Intake model form shows the form, with a drop-down field to resource, with a caveat that it shows all resources, regardless of previous allocation or not.
ex. if resource is taken by an intake, save button detects that disallow saves. This is expected, but then the drop-down should not show that resource in the first place.
How can I do this, i.e. do not show resource already allocated ?
class Resource(models.Model):
label = models.CharField(max_length=50, primary_key=True)
created = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey('auth.User', default=1)
class Meta:
verbose_name_plural = "Resource Pool"
def __str__(self):
return self.label
class Intake(models.Model):
order_id = models.AutoField(primary_key=True)
requestor = models.ForeignKey('auth.User', default=1)
resource = models.OneToOneField(Resource, verbose_name="Allocation")
project = models.CharField(max_length=50)
class Meta:
verbose_name_plural = "Environment Request"
def __str__(self):
print("self called")
return self.project
You can create a custom form in your admin and change the queryset value of the resource field. Something like this:
admin.py
from django import forms
from django.db.models import Q
from .models import Intake
class IntakeForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(IntakeForm, self).__init__(*args, **kwargs)
self.fields['resource'].queryset = Resource.objects.filter(
Q(intake__isnull=True) | Q(intake=self.instance)
)
class IntakeAdmin(admin.ModelAdmin):
form = IntakeForm
admin.site.register(Intake, IntakeAdmin)
You could probably use limit_choices_to on the field definition:
resource = models.OneToOneField(Resource, verbose_name="Allocation",
limit_choices_to={'intake__isnull': True})

Complicated "limit_choices_to" function in Django

I have Django database with 2 models: DeviceModel and Device. Let's say, for example, DeviceModel object is "LCD panel" and Device object is "LCD panel №547". So these two tables have ManyToOne relationship.
class DeviceModel(models.Model):
name = models.CharField(max_length=255)
class Device(models.Model):
device_model = models.ForeignKey(DeviceModel)
serial_number = models.CharField(max_length=255)
Now I need to add some relations between DeviceModel objects. For example "LCD Panel" can be in "Tablet" object or in "Monitor" object. Also another object can be individual, so it doesn't link with other objects.
I decided to do this with ManyToMany relationship, opposed to using serialization with JSON or something like that (btw, which approach is better in what situation??).
I filled all relationships between device models and know I need to add relationship functional to Device table.
For that purpose I added "master_dev" foreignkey field pointing to 'self'. It works exactly as I need, but I want to restrict output in django admin panel. It should display only devices, that are connected through device_links. Current code:
class DeviceModel(models.Model):
name = models.CharField(max_length=255)
device_links = models.ManyToManyField('self')
class Device(models.Model):
device_model = models.ForeignKey(DeviceModel)
serial_number = models.CharField(max_length=255)
master_dev = models.ForeignKey('self', blank=True, null=True)
So, how can I limit output of master_dev field in admin panel?
There is a function "limit_choices_to", but I can't get it to work...
in forms.py:
def master_dev_chioses():
chioses = DeviceModel.objects.filter(do your connection filter here - so not all Devicemodels comes to choicefield)
class DeviceForm(forms.ModelForm):
class Meta:
model = Device
def __init__(self, *args, **kwargs):
super(Device, self).__init__(*args, **kwargs)
self.fields['master_dev'].choices = master_dev_chioses()
While there is no direct answer to my question about "limit_choices_to" function, I post solution that achieves desired output:
from django import forms
from django.contrib import admin
from .models import DeviceModel, Device
class DeviceForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(DeviceForm, self).__init__(*args, **kwargs)
try:
linked_device_models = self.instance.device_model.device_links.all()
linked_devices = Device.objects.filter(device_model__in=linked_device_models)
required_ids = set(linked_devices.values_list("id", flat=True))
self.fields['master_dev'].queryset = Device.objects.filter(id__in=required_ids).order_by("device_model__name", "serial_number")
except:
# can't restrict masters output if we don't know device yet
# admin should edit master_dev field only after creation
self.fields['master_dev'].queryset = Device.objects.none()
class Meta:
model = Device
fields = ["device_model", "serial_number", "master_dev"]
class DeviceAdmin(admin.ModelAdmin):
form = DeviceForm
list_display = ('id', 'device_model', 'serial_number')
list_display_links = ('id', 'device_model')
search_fields = ('device_model__name', 'serial_number')
list_per_page = 50
list_filter = ('device_model',)

M2M using through and form with multiple checkboxes

I'd like to create a form allowing me to assign services to supplier from these models. There is no M2M relationship defined since I use a DB used by others program, so it seems not possible to change it. I might be wrong with that too.
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
contact = models.ForeignKey(Contact, null=False, blank=False)
class SupplierPrice(models.Model):
service_user = models.ForeignKey('ServiceUser')
price_type = models.IntegerField(choices=PRICE_TYPES)
price = models.DecimalField(max_digits=10, decimal_places=4)
I've created this form:
class SupplierServiceForm(ModelForm):
class Meta:
services = ModelMultipleChoiceField(queryset=Service.objects.all())
model = ServiceUser
widgets = {
'service': CheckboxSelectMultiple(),
'contact': HiddenInput(),
}
Here is the view I started to work on without any success:
class SupplierServiceUpdateView(FormActionMixin, TemplateView):
def get_context_data(self, **kwargs):
supplier = Contact.objects.get(pk=self.kwargs.get('pk'))
service_user = ServiceUser.objects.filter(contact=supplier)
form = SupplierServiceForm(instance=service_user)
return {'form': form}
I have the feeling that something is wrong in the way I'm trying to do it. I have a correct form displayed but it is not instantiated with the contact and checkboxes aren't checked even if a supplier has already some entries in service_user.
You are defining services inside your Meta class. Put it outside, right after the beginning of SupplierServiceForm. At the very least it should show up then.
Edit:
I misunderstood your objective. It seems you want to show a multiple select for a field that can only have 1 value. Your service field will not be able to store the multiple services.
So, by definition, your ServiceUser can have only one Service.
If you don't want to modify the database because of other apps using it, you can create another field with a many to many relationship to Service. That could cause conflicts with other parts of your apps using the old field, but without modifying the relationship i don't see another way.
The solution to my problem was indeed to redefine my models in oder to integrate the m2m relationship that was missing, using the through argument. Then I had to adapt a form with a special init method to have all selected services displayed in checkboxes, and a special save() method to save the form using m2m relationship.
class Supplier(Contact):
services = models.ManyToManyField('Service', through='SupplierPrice')
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
supplier = models.ForeignKey(Supplier, null=False, blank=False)
price = models.Decimal(max_digits=10, decimal_places=2, default=0)
And the form, adapted from the very famous post about toppings and pizza stuff.
class SupplierServiceForm(ModelForm):
class Meta:
model = Supplier
fields = ('services',)
widgets = {
'services': CheckboxSelectMultiple(),
'contact_ptr_id': HiddenInput(),
}
services = ModelMultipleChoiceField(queryset=Service.objects.all(), required=False)
def __init__(self, *args, **kwargs):
# Here kwargs should contain an instance of Supplier
if 'instance' in kwargs:
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
initial = kwargs.setdefault('initial', {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data (checked boxes).
initial['services'] = [s.pk for s in kwargs['instance'].services.all()]
ModelForm.__init__(self, *args, **kwargs)
def save(self, commit=True):
supplier = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
def save_m2m():
new_services = self.cleaned_data['services']
old_services = supplier.services.all()
for service in old_services:
if service not in new_services:
service.delete()
for service in new_services:
if service not in old_services:
SupplierPrice.objects.create(supplier=supplier, service=service)
self.save_m2m = save_m2m
# Do we need to save all changes now?
if commit:
self.save_m2m()
return supplier
This changed my first models and will make a mess in my old DB but at least it works.

How to hide model field in Django Admin?

I generate field automaticly, so I want to hide it from user. I've tried editable = False and hide it from exclude = ('field',). All this things hide this field from me, but made it empty so I've got error: null value in column "date" violates not-null constraint.
models.py:
class Message(models.Model):
title = models.CharField(max_length = 100)
text = models.TextField()
date = models.DateTimeField(editable=False)
user = models.ForeignKey(User, null = True, blank = True)
main_category = models.ForeignKey(MainCategory)
sub_category = models.ForeignKey(SubCategory)
groups = models.ManyToManyField(Group)`
admin.py:
class MessageAdminForm(forms.ModelForm):
def __init__(self, *arg, **kwargs):
super(MessageAdminForm, self).__init__(*arg, **kwargs)
self.initial['date'] = datetime.now()
class MessageAdmin(admin.ModelAdmin):
form = MessageAdminForm
list_display = ('title','user',)
list_filter = ('date',)
Based on your model setup, I think the easiest thing to do would change your date field to:
date = models.DateTimeField(auto_now=True)
that should accomplish what you're after and you don't even need to exclude it from the admin, it's excluded by default. If you have auto_now=True it will act as a 'last update time'. If you have auto_now_add=True it will act as a creation time stamp.
There are several other ways you could accomplish your goal if your use case is more complex than a simple auto date field.
Override the model's save method to put the value in.
class Message(models.Model):
title=models.CharField(max_length=100)
date = models.DateTimeField(editable=False)
def save(*args, **kwargs):
self.date = datetime.datetime.now()
super(Message, self).save(*args, **kwargs)
What you are trying to do with the Model Admin isn't quite working because by default django only transfers the form fields back to a model instance if the fields are included. I think this might be so the model form doesn't try to assign arbitrary attributes to the model. The correct way to accomplish this would be to set the value on the instance in your form's save method.
class MessageAdminForm(forms.ModelForm):
def save(*args, **kwargs):
self.instance.date = datetime.now()
return super(MessageAdminForm, self).save(*args, **kwargs)