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')
)
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 am trying to have a custom form on django admin for my ModelB, with fields taken from other ModelA.
models.py
class ModelA(models.Model):
source = models.CharField(max_length=80)
keys = ArrayField(
models.CharField(max_length=50)
)
class ModelB(models.Model):
characteristic_keys = JSONField()
forms.py
class ModelBForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
queryset = ModelA.objects.all()
dynamic_fields = [(x.source, x.keys) for x in queryset]
# New fields to be shown on admin =>
# Field name => "source" from modelA
# Field type => multiple choice with options => "keys" from modelA
for field in dynamic_fields:
self.fields[field[0]] = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple,
choices=field[1])
def save(self, commit=True):
# ...do something with extra_field here...
return super().save(commit=commit)
class Meta:
model = Workflow
fields = "__all__"
admin.py
class ModelBAdmin(admin.ModelAdmin):
form = ModelBForm
admin.site.register(ModelB, ModelBAdmin)
I want a single form for ModelB on django admin, with dynamic "source" fields takes from ModelA, with multiple choice options from their corresponding "key" values in modelB.
I have tried to keep information clear and understandable, please let me know if I have missed any information that might be needed to understand the problem. Any ideas to deal this problem would be a great help!
Using a custom ModelForm in my default (django admin) change view gives me an empty variable self.fields if the form gets rendered for a user with only view permissions (new in Django 2.1).
This is my code:
# models.py
class Door(ValidateOnSaveMixin, models.Model):
...
motor_type = models.ForeignKey(
MotorType,
on_delete=models.SET_NULL,
default=None,
blank=True,
null=True)
...
door_type = models.CharField(
max_length=3,
choices=DOOR_TYPES,
null=True,
default=None)
...
vehicle_variant = models.ForeignKey(
VehicleVariant,
on_delete=models.CASCADE)
class Meta:
unique_together = ("vehicle_variant", "location", "motor_type")
...
# admin.py
#admin.register(Door)
class DoorAdmin(ImportExportModelAdmin):
form = DoorAdminForm
list_display = ('descriptor', 'get_customer_link', 'get_variant', 'location', 'get_motor_type_link',
'window_type', 'door_type', 'drum_diameter', 'dist_per_motor_rotation')
fields = ('vehicle_variant', 'description', 'location', 'motor_type',
'drum_diameter', 'window_type', 'door_type')
...
# forms.py
class DoorAdminForm(ModelForm):
class Meta:
model = Door
fields = '__all__'
widgets = {
'motor_type': DoorMotorTypeWidget,
}
def __init__(self, *args, **kwargs):
super(DoorAdminForm, self).__init__(*args, **kwargs)
# this line is crashing on access with a user who has only the view permission, as self.fields is empty
self.fields['vehicle_variant'].queryset = VehicleVariant.objects.all().prefetch_related('customer').order_by('customer__name', 'name')
The root cause is related to the exclude attribute of the Meta class in DoorAdminForm.
No matter what i write to the fields/exclude attributes, always all of the model's fields get automatically put to the exclude list and prevent self.fields to be populated. And this makes my code crash.
Of course i can check for 'verhicle_variant' in self.fields but i don't understand the reason for that behaviour. I also couldn't find the part in the django source which populates the exclude attribute with all model fields.
Anybody has an idea if this is intended? Whats the root cause for that behaviour (ignoring the fields and exclude attribute of the Meta class)?
The point where all fields get put into exclude is here:
https://github.com/django/django/blob/cf79f92abee2ff5fd4fdcc1a124129a9773805b8/django/contrib/admin/options.py#L674
I have never used django admin with just 'view' permissions. Haven't really found a lot of information about how this should look like either: django-admin comes with add- and change-views amongst others, but no viewonly-view? Almost looks like it doesn't really know what to do with users that do not have 'change' permission. It directs them to the change_view and then notices that they are not allowed to change anything so it displays an empty form?
I suppose you could just declare all the fields as readonly for a viewonly-view?
UPDATE:
The field vehicle_variant won't be in form.fields as it would still be excluded due to not having change permission, but at least you don't crash on the form's init. I still have no idea what a change_view without change permissions should look like other than everything being readonly.
class DoorAdminForm(ModelForm):
qs = VehicleVariant.objects.prefetch_related('customer').order_by('customer__name', 'name')
model_field = Door._meta.get_field('vehicle_variant')
vehicle_variant = model_field.formfield(queryset=qs)
class Meta:
model = Door
fields = '__all__'
widgets = {
'motor_type': DoorMotorTypeWidget,
}
I have a model called Keyword and a model called Statement, and I've been customizing the form for adding or changing statements. Each Keyword object has an m2m (many to many) relationship with Statement objects, and I wanted users to be able to select keywords to associate. The default widget for m2m fields isn't useful in my case because there are many Keyword objects so I needed something better than that. I used the FilteredSelectMultiple widget in order to get the adjustments I needed.
Here's the code for that.
In admin.py
class KeywordInline(admin.TabularInline):
model = Keyword.statement.through
class StatementAdmin(admin.ModelAdmin):
list_display = ('statement_id', 'title', 'author', 'released_by', 'issue_date', 'access', 'full_text',)
list_filter = (StatementListFilter, 'released_by', 'issue_date', 'access',)
search_fields = ('statement_id', 'title', 'author', 'issue_date',)
inlines = [ KeywordInline,]
in forms.py
class StatementForm(forms.Modelform):
statement_keywords = forms.ModelMultipleChoiceField(
queryset=Keyword.objects.all(),
required=False,
widget=FilteredSelectMultiple(
verbose_name='Keywords Associated with Statement',
is_stacked=False
)
)
class Meta:
model = Statement
def __init__(self, *args, **kwargs):
super(StatementForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.fields['statement_keywords'].initial = self.instance.keyword_set.all()
def save(self, commit=True):
statement = super(StatementForm, self).save(commit=False)
if commit:
statement.save()
if statement.pk:
statement.keyword_set = self.cleaned_data['keyword']
self.save_m2m()
return statement
So now I have a filter_horizontal menu for my inline, just like I wanted. But there's one problem: There's no plus sign to add new keywords.
I know that the RelatedFieldWidgetWrapper is necessary to resolve this, and I've found tons of examples of people using it. However, I haven't been able to find one that suits my situation. The most immediate problem I'm having right now is trying to insert something into the "rel" parameter. The "rel" parameter typically defines "a relation of the two models involved," going off of this popular example implementation: http://dashdrum.com/blog/2012/07/relatedfieldwidgetwrapper/
I don't know what to indicate for this relation nor how to indicate it because I'm working with an inline. So I'm not actually working with a field called "keywords," I am doing a reverse look up of the m2m relationship between Keyword and Statement. So I don't know what the name could be to describe the relationship.
All of the examples I've found haven't really talked about what to do in this situation. Most examples easily get the field of interest from one of the models and then get its type or relationship, but with an inline model and a reverse relation I can't necessarily do that.
I managed to make ends meet with a many to many relation and a custom widget of an inline model, like the one you are describing.
Inspired from this answer and this post here is the result using my models because you do not provide a models.py in your question and you also have extra -unnecessary for this occasion- information in your code.
models.py
class MasterModel(models.Model):
pass
class ThirdModel(models.Model):
pass
class InlineModel(models.Model):
'''
It should also be working with a ForeignKey
but I have not tested it.
'''
master_key = models.OneToOneField(MasterModel)
the_field = models.ManyToManyField(ThirdModel)
forms.py
from django.contrib.admin import site as admin_site
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
class InlineModelForm(forms.Modelform):
the_field = forms.ModelMultipleChoiceField(
queryset=ThirdModel.objects.all(),
required=False,
widget=(
<the_custom_widget with attributes etc>
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['the_field'].widget = (
RelatedFieldWidgetWrapper(
self.fields['the_field'].widget,
self.instance._meta.get_field('the_field').rel,
admin_site,
)
)
class Meta:
model = InlineModel
fields = '__all__'
admin.py:
class InlineModelAdminInline(admin.TabularInline):
model = InlineModel
form = InlineModelForm
#admin.register(MasterModel)
class MasterModelAdmin:
inlines = (InlineModelAdminInline, )
#admin.register(InlineModel)
class InlineModelAdmin:
form = InlineModelForm
#admin.register(ThirdModel)
class ThirdModelAdmin:
pass
I'm using Django 1.2's new ManyToMany admin.TabularInline to display related objects in the admin app, and it works great except I can't figure out what to set the "ordering" property to so it can sort by one of the cross-referenced field names.
For instance:
class Foo(models.Model):
name = models.CharField(max_length=100)
class Bar(models.Model):
title = models.CharField(max_length=100)
foos = models.ManyToManyField(Foo)
class FooBarInline(admin.TabularInline):
model = Bar.foos.through
ordering = ('name', ) # DOES NOT WORK
raw_id_fields = ('name', ) # THROWS EXCEPTION
class FooAdmin(admin.ModelAdmin):
inlines = (FooBarInline, )
class Meta:
model = Foo
How can I get to the Foo.name field to order by it in the inline?
The model ordering meta option designates the order of the inline elements.
class Foo(models.Model):
name = models.CharField(max_length=100)
class Meta:
ordering = ('name',)
If you needed to have the ordering of the admin model different from your primary ordering, you could do something like this:
class Foo_Extended(Foo):
class Meta:
ordering = ('name',)
And use Foo_Extended for your AdminInline model.
I'm assuming you know this, but Django 1.3 adds and ordering option to the InlineAdmin model but I know you said Django 1.2
I think you may override
ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs)
You can find details in the docs for ModelAdmin.formfield_for_foreignkey.