I have two models:
class Author(models.Model):
name = models.CharField(max_length=100)
create_report = models.BooleanField(default=False)
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.PROTECT)
title = models.CharField(max_length=100)
They are registered in admin like this:
class BookInline(admin.TabularInline):
model = PurchaseOrderItem
#admin.register(PurchaseOrder)
class AuthorAdmin(admin.ModelAdmin):
inlines = (PurchaseOrderInline,)
I create an author and two books through Django admin. After I hit the 'Save' button, if Author.create_report == True I would like to see a report saying the following:
Author Whoever-he-is has written the following books:
Title-of-the-first-book
Title-of-the-first-book
(Where the report should appear or how to render the template are not relevant questions here, let's skip them.)
My first idea was to overwrite Author.save() method:
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.create_report:
self.write_report(name=self.name, books=self.book_set.all())
The problem is that Author.save() method is executed before Book objects are saved and so self.book_set.all() is empty.
One workaround would be to use some other ModelAdmin function (for instance log_addition()), which fires after Book objects are saved, but as I wish to use the same functionality out of admin, too, it is not the best solution.
Can I somehow achieve the result without using the admin layer functions?
The book_set.all() will be Null initially, because there is no Book related to the Author at the time of author creation. So, what I'm suggesting is, generate the report whenever a Book is created.
class Author(models.Model):
name = models.CharField(max_length=100)
create_report = models.BooleanField(default=False)
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.PROTECT)
title = models.CharField(max_length=100)
def save(self, *args, **kwargs):
pk = self.pk # pk will be None like objects if self is new instance
super().save(*args, **kwargs)
if not pk and self.author.create_report:
write_report(name=self.author.name, books=self.author.book_set.all())
I am having a similar issue. I think this is exactly the reason why Django introduced signals. You can fire and catch a signal of the created Books/Authors and then fire an action from there that creates some sort of report.
But I get your point (since I am feeling the same pain) ... id would be great is this would work straight from the model.
Related
SOLUTION AT THE BOTTOM
Problem: Django form populating with list of objects rather than values
Summary: I have 2 models Entities and Breaks. Breaks has a FK relationship to the entity_id (not the PK) on the Entities model.
I want to generate an empty form for all the fields of Breaks. Generating a basic form populates all the empty fields, but for the FK it generates a dropdown list of all objects of the Entities table. This is not helpful so I have excluded this in the ModelForm below and tried to replace with a list of all the entity_ids of the Entities table. This form renders as expected.
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
exclude = ('entity',)
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all().values_list('entity_id', flat=True))
The below FormView is the cbv called by the URL. As the below stands if I populate the form, and for the FK column entity_id choose one of the values, the form will not submit. By that field on the form template the following message appears Select a valid choice. That choice is not one of the available choices.
class ContactFormView(FormView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
My initial thoughts were either that the datatype of this field (string/integer) was wrong or that Django needed the PK of the row in the Entities table (for whatever reason).
So I added a post function to the FormView and could see that the request.body was populating correctly. However I can't work out how to populate this into the ModelForm and save to the database, or overcome the issue mentioned above.
Addendum:
Models added below:
class Entity(models.Model):
pk_securities = models.AutoField(primary_key=True)
entity_id = models.CharField(unique=True)
entity_description = models.CharField(blank=True, null=True)
class Meta:
managed = False
db_table = 'entities'
class Breaks(models.Model):
pk_break = models.AutoField(primary_key=True)
date = models.DateField(blank=True, null=True)
entity = models.ForeignKey(Entity, on_delete= models.CASCADE, to_field='entity_id')
commentary = models.CharField(blank=True, null=True)
active = models.BooleanField()
def get_absolute_url(self):
return reverse(
"item-update", args=[str(self.pk_break)]
)
def __str__(self):
return f"{self.pk_break}"
class Meta:
managed = False
db_table = 'breaks'
SOLUTION
Firstly I got this working by adding the following to the Entity Model class. However I didn't like this as it would have consequences elsewhere.
def __str__(self):
return f"{self.entity_id}"
I found this SO thread on the topic. The accepted answer is fantastic and the comments to it are helpful.
The solution is to subclass ModelChoiceField and override the label_from_instance
class EntityChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.entity_id
I think your problem is two fold, first is not rendering the dropdown correctly and second is form is not saving. For first problem, you do not need to do any changes in ModelChoiceField queryset, instead, add to_field_name:
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all(), to_field_name='entity_id')
Secondly, if you want to save the form, instead of FormView, use CreateView:
class ContactFormView(CreateView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
model = Breaks
In Django, the request object passed as parameter to your view has an attribute called "method" where the type of the request is set, and all data passed via POST can be accessed via the request. POST dictionary. The view will display the result of the login form posted through the loggedin. html.
I have a model that needs to include the ID in another field when its created... see below:
id = models.AutoField(primary_key=True)
ticket_number = models.CharField(max_length=200, blank=True)
brand = models.ForeignKey(Brand, on_delete=models.SET_NULL, null=True)
...
def save(self, *args, **kwargs):
shortname = self.brand.shortname
super(Job, self).save(*args, **kwargs)
self.ticket_number = shortname.upper() + '-' + str(self.id)
super(Job, self).save(*args, **kwargs)
It works, which is good, and creates a unique ticket number. I'm new to Django, but I know enough that I feel like saving a post twice is inefficient. Is there something like .save(commit=False) for Models? I'd like to only save it once per save.
You can't really know in advance what self.id will be until the corresponding database row is created and Django extracts it from there. If you aren't planning on filtering on the column, you can just turn it into a property that's computed when you look it up on a model instance:
class Thing(db.Model):
id = models.AutoField(primary_key=True)
brand = models.ForeignKey(Brand, on_delete=models.SET_NULL, null=True)
#property
def ticket_number(self):
return '{}-{}'.format(self.brand.shortname.upper(), self.id)
You can use the post_save signal, to update the ticket number. The post_save signal gets called after your model is saved, hence it will have an id. Although, you will still be performing two saves, but you will only call save explicitly once.
Post save documentation: here
That will hopefully give you more mental peace =).
I have a model with two entities, Person and Code. Person is referenced by Code twice, a Person can be either the user of the code or the approver.
What I want to achieve is the following:
if the user provides an existing Person.cusman, no further action is needed.
if the user provides an unknown Person.cusman, a helper code looks up other attributes of the Person (from an external database), and creates a new Person entity.
I have implemented a function triggered by pre_save signal, which creates the missing Person on the fly. It works fine as long as I use python manage.py shell to create a Code with nonexistent Person.
However, when I try to add a new Code using the admin form or a CreateView descendant I always get the following validation error on the HTML form:
Select a valid choice. That choice is not one of the available choices.
Obviously there's a validation happening between clicking on the Save button and the Code.save() method, but I can't figure out which is it. Can you help me which method should I override to accept invalid foreign keys until pre_save creates the referenced entity?
models.py
class Person(models.Model):
cusman = models.CharField(
max_length=10,
primary_key=True)
name = models.CharField(max_length=30)
email = models.EmailField()
def __unicode__(self):
return u'{0} ({1})'.format(self.name, self.cusman)
class Code(models.Model):
user = models.ForeignKey(
Person,
on_delete=models.PROTECT,
db_constraint=False)
approver = models.ForeignKey(
Person,
on_delete=models.PROTECT,
related_name='approves',
db_constraint=False)
signals.py
#receiver(pre_save, sender=Code)
def create_referenced_person(sender, instance, **kwargs):
def create_person_if_doesnt_exist(cusman):
try:
Person = Person.objects.get(pk=cusman)
except Person.DoesNotExist:
Person = Person()
cr = CusmanResolver()
Person_details = cr.get_person_details(cusman)
Person.cusman = Person_details['cusman']
Person.name = Person_details['name']
Person.email = Person_details['email']
Person.save()
create_Person_if_doesnt_exist(instance.user_id)
create_Person_if_doesnt_exist(instance.approver_id)
views.py
class CodeAddForm(ModelForm):
class Meta:
model = Code
fields = [
'user',
'approver',
]
widgets = {
'user': TextInput,
'approver': TextInput
}
class CodeAddView(generic.CreateView):
template_name = 'teladm/code_add.html'
form_class = CodeAddForm
You misunderstood one thing: You shouldn't use TextField to populate ForeignKey, because django foreign keys are populated using dropdown/radio button to refer to the id of the object in another model. The error you got means you provided wrong information that doesn't match any id in another model(Person in your case).
What you can do is: not using ModelForm but Form. You might have some extra work to do after you call form.is_valid(), but at least you could code up your logic however you want.
If I have two models in Django application like this:
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
author = models.ForeignKey(Author)
title = models.CharField(max_length=100)
How can I create a single form that allows you add both an Author and a Book simultaneously. If the author exists in the system, I could simply display the book Form and link that to the author but it is very often that I need to allow my users to create the book and the author simultaneously.
How can I do this?
Thanks.
You can write a custom form, which will check if the author exists in the system use existing, if no, create new with provided name.
class CustomForm(forms.ModelForm):
author = forms.CharField()
def save(self, commit=True):
author, created = Author.objects.get_or_create(name=self.cleaned_data['author'])
instance = super(CustomForm,self).save(commit=commit)
instance.author = author
if commit:
instance.save()
return instance
class Meta:
model=Book
Not sure this code is working, but I suppose it can explain my idea.
You can create a view that handles multiple forms - see http://collingrady.wordpress.com/2008/02/18/editing-multiple-objects-in-django-with-newforms/ for an excellent example.
You'd have to ensure that the rendering of the form objects are done in the template with only one tag and one submit button.
Here's my setup:
from django.contrib.auth.models import User
class Product(models.Model):
...
email_users = models.ManyToManyField(User, null=True, blank=True)
...
[elsewhere]
class ProductAdmin(admin.ModelAdmin):
list_display = ('name','platform')
admin.site.register(Product, ProductAdmin)
My main problem is, when I'm viewing the "Product" page in the admin section, email users are not being being ordered by their ID by default, and I'd like that to be ordered by their username.
From what I've read so far, it seems like I need to be adding:
email_users.admin_order_field = 'xxxx'
But I'm not quite sure what the syntax is to access the username.
The answer was referred to in Hao Lian's comment above, essentially, this is what needed to be done:
class ProductAdminForm(ModelForm):
email_users = forms.ModelMultipleChoiceField(queryset=User.objects.order_by('username'))
class Meta:
model = Product
class ProductAdmin(admin.ModelAdmin):
list_display = ('name','platform')
form = ProductAdminForm
admin.site.register(Product, ProductAdmin)
Mine was slightly different in the sense that I required the forms.ModelMultipleChoiceField, whereas the answer provided used forms.ModelChoiceField()
Solution above works well, but in my case I lost all attributes and customizations that my component had by default (like required, label, etc).
In some cases could be better override __init__() method in order to customize only your queryset, nothing else will change.
class ProductAdminForm(ModelForm):
class Meta:
model = Product
fields = '__all__' # required in new versions
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['email_users'].queryset = (
self.fields['email_users'].queryset.order_by('username')
)