I have a request from a client to have page admin fields that they can add/read numbers into with commas such as 1,000,000.
The Django model field to store the value would be a django.db.models.fields.DecimalField instance.
From looking at the Django docs, this is something that’s supported by the django.forms.fields.DecimalField localized property, but I can’t find a way of enforcing it in the Wagtail admin, even when subclassing the Wagtail BaseFieldPanel __init__ function with self.bound_field.field.localize = True.
You can override the fields that Wagtail FieldPanel uses by customising generated forms documented here:
http://docs.wagtail.io/en/v1.13/advanced_topics/customisation/page_editing_interface.html#wagtail.wagtailadmin.forms.WagtailAdminPageForm
Basic example below - myapp/models.py
from django import forms
from django.db import models
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
from wagtail.wagtailcore.models import Page
class MyCustomPageForm(WagtailAdminPageForm):
# fields here are django.forms fields
# when set to localize, renders as a TextInput widget
total_amount = forms.DecimalField(localize=True)
# when left with defaults, renders as a NumberInput widget
# total_amount = forms.DecimalField()
# can also set any kind of widget here
# total_amount = forms.DecimalField(widget=MyCustomDecimalWidget)
class MyCustomPage(Page):
# fields here are django.db.models fields
total_amount = models.DecimalField()
content_panels = Page.content_panels + [
FieldPanel('total_amount'),
]
base_form_class = MyCustomPageForm # important: must set this
When you set the form field to localize=True it appears to follow the intended behaviour as per the Django Docs which is to render a TextInput widget.
You could also set your own widget for any field in your page's form class if you want to do some more complicated functionality.
Thanks #lb-ben-johnston - also to the Wagtail team who pointed me in a similar direction.
I have it working now in a loosely coupled fashion with this:
from django.forms.fields import DecimalField
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
class ProductPageForm(WagtailAdminPageForm):
def __init__(self, *args, **kwargs):
super(ProductPageForm, self).__init__(*args, **kwargs)
for key, field in self.fields.items():
if isinstance(field, DecimalField):
field.localize = True
field.widget.is_localized = True
# The Page class to localize
class LocalizedPage(Page):
base_form_class = ProductPageForm
# Rest of page class
Make sure you explictly pass a TextInput widget to the field handler:
FieldPanel('localized_field', widget=TextInput)
Finally, make sure that the thousand separator flag is explicitly set in your settings.py or base.py:
USE_THOUSAND_SEPARATOR = True
Related
I have a wagtail site and have a problem with the ‘users’ section of the admin page
My users/admin.py is :
from django.contrib import admin
from django.contrib.auth import admin as auth_admin
from django.contrib.auth import get_user_model
from psymatik.users.forms import (
UserChangeForm,
UserCreationForm,
)
User = get_user_model()
admin.site.register(User)
class UserAdmin(auth_admin.UserAdmin):
form = UserChangeForm
add_form = UserCreationForm
fieldsets = (
("User", {"fields": ("username",)}),
) + auth_admin.UserAdmin.fieldsets
list_display = ["username", "is_superuser"]
search_fields = ["username"]
And my users/wagtail_hooks.py is:
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from .models import User
class UserAdmin(ModelAdmin):
model = User
menu_label = "Users"
menu_icon = "pick"
menu_order = 200
add_to_settings_menu = False
exclude_from_explorer = False
list_display = ( "name")
list_filter = ("name")
search_fields = ("name")
modeladmin_register(UserAdmin)
The issues I have is that when I am at admin/users and I click on the Users link in the sidebar I am taken to admin/users/user and get the error
“ValueError at /admin/users/user/
Field 'id' expected a number but got 'user’.”
Why is the sidebar link pointing to admin/users/user rather than just admin/users (which does work)? What is the best way to set this up?
When you register a model with ModelAdmin, the URLs will be formed from the app name ('users' here) and the model name ('user'), so /admin/users/user is expected. However, in this case Wagtail already provides a user management area (available from the Settings submenu) that exists under the /admin/users/ URL namespace - these URLs end up colliding with the ones you add through ModelAdmin.
It looks like your UserAdmin definition is more or less duplicating the functionality already provided by Wagtail, so you may not need this at all. If you do, one thing that might work is editing the INSTALLED_APPS setting in your project's settings to move your users app above wagtail.contrib.users - that way, the URL patterns for your own app will take precedence over Wagtail's built in /admin/users/ area, and it will correctly interpret anything under /admin/users/user/ as belonging to your own app (while letting all other URLs under /admin/users/ fall back to the Wagtail built-in area).
If that doesn't work, you'd need to either rename your users app (easier said than done for an established project...) or customise the ModelAdmin setup to use an alternative URL path. It doesn't look like ModelAdmin currently provides an official mechanism to do that, but overriding the AdminURLHelper object ought to work - within users/wagtail_hooks.py:
from wagtail.contrib.modeladmin.helpers import AdminURLHelper
class UsersAdminURLHelper(AdminURLHelper):
def _get_action_url_pattern(self, action):
# hard-code 'user-accounts' as the URL path instead of /users/user/
if action == "index":
return r"^user-accounts/$"
return r"^user-accounts/%s/$" % action
class UserAdmin(ModelAdmin):
model = User
url_helper_class = UsersAdminURLHelper
# all other settings as before
Incidentally, users/admin.py is unrelated here - it controls the Django admin backend, which is distinct from the Wagtail admin.
I have 5 models and their relations are as follows:
class A(models.Model):
pass
class B(models.Model):
a = models.ForeignKey(A)
class C(models.Model):
b = models.ManyToManyField(B)
class D(models.Model):
pass
class I(models.Model):
a = models.ForeignKey(A)
b = models.ForeignKey(B)
c = models.ForeignKey(C)
d = models.ForeignKey(D)
I decide to use the django admin
class IAdminInline(admin.TabularInline):
pass
class DAdmin(admin.ModelAdmin):
inlines = [IAdminInline, ]
The admin page makes a lot queries,if too many I instances are related to D, which is time consuming. So I disable the Django default actions by setting the formfield_for_foreignkey:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
field = super().formfield_for_foreignkey(db_field, request, **kwargs)
field.choices = [] # django will not make any queries if set choices
Instead, I use ajax to get the corresponding data, and use javascript to render the select widgets and bind actions , which make it easier to add data since these widgets are related to each other. Page loads faster but problem is that, the above code would clear I instances initial values that are apparently already existing in the change view page.
I want to ask how can I render the existing inline object select widgets to their own values? Does Django provide any functions to handle this?
I haven't find any solutions other than using ajax to render the apparently existing values all by myself.
I'm trying to replace the standard AdminSplitDateTime widget in my admin site for better functionality (basically I want to display only 'available' dates in my calender which I couldn't find how to do with the default picker).
I decided to use the bootstrap3_datetime widget.
After overriding my field to use the new widget, it doesn't seem to be transferred into the 'clean' method (isn't in self.cleaned_data) for validation.
models.py
publish_time = models.DateTimeField('Date to publish')
admin.py
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
bad_dates = []
#populating bad_dates with application logic
def clean(self):
# This will always return None when using the new widget.
# When working with the default widget, I have the correct value.
publish_time = self.cleaned_data.get('publish_time', None)
publish_time = forms.DateTimeField(widget=DateTimePicker(options=
{"format": "DD-MM-YYYY HH:mm",
"startDate": timezone.now().strftime('%Y-%m-%d'),
"disabledDates": bad_dates,
})
class MyModelAdmin(admin.ModelAdmin):
form = MyForm
admin.site.register(MyModel, MyModelAdmin)
HTML-wise, the widget works well and the text field is populated with the correct date (and with the 'bad_dates' disabled). The problem is that it seems it isn't saved on the form.
I also tried initializing the widget in the init method by doing:
self.fields['publish_time'].widget = DateTimePicker(options=...)
But the result was the same.
What am I missing here?
Is it even possible to modify widgets in the admin site?
Thanks!
Update:
I've analysed the POST request that is sent using each of the widgets.
In the default admin widget, I see that it generates two fields: "publish_time_0" (for date) and "publish_time_1" (for time).
In the bootstrap3 widget, only a single "publish_time" field is sent.
I'm assuming that the admin site understands that the field is a DateTimeField (from models), looks for id_0 and id_1 and that's why it fails.
Does that make sense? Is there anyway around it?
I have 2 models - for example, Book and Page.
Page has a foreign key to Book.
Each page can be marked as "was_read" (boolean), and I want to prevent deleting pages that were read (in the admin).
In the admin - Page is an inline within Book (I don't want Page to be a standalone model in the admin).
My problem - how can I achieve the behavior that a page that was read won't be deleted?
I'm using Django 1.4 and I tried several options:
Override "delete" to throw a ValidationError - the problem is that the admin doesn't "catch" the ValidationError on delete and you get an error page, so this is not a good option.
Override in the PageAdminInline the method - has_delete_permission - the problem here -it's per type so either I allow to delete all pages or I don't.
Are there any other good options without overriding the html code?
Thanks,
Li
The solution is as follows (no HTML code is required):
In admin file, define the following:
from django.forms.models import BaseInlineFormSet
class PageFormSet(BaseInlineFormSet):
def clean(self):
super(PageFormSet, self).clean()
for form in self.forms:
if not hasattr(form, 'cleaned_data'):
continue
data = form.cleaned_data
curr_instance = form.instance
was_read = curr_instance.was_read
if (data.get('DELETE') and was_read):
raise ValidationError('Error')
class PageInline(admin.TabularInline):
model = Page
formset = PageFormSet
You could disable the delete checkbox UI-wise by creating your own custom
formset for the inline model, and set can_delete to False there. For
example:
from django.forms import models
from django.contrib import admin
class MyInline(models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyInline, self).__init__(*args, **kwargs)
self.can_delete = False
class InlineOptions(admin.StackedInline):
model = InlineModel
formset = MyInline
class MainOptions(admin.ModelAdmin):
model = MainModel
inlines = [InlineOptions]
Another technique is to disable the DELETE checkbox.
This solution has the benefit of giving visual feedback to the user because she will see a grayed-out checkbox.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
def add_fields(self, form, index):
super().add_fields(form, index)
if some_criteria_to_prevent_deletion:
form.fields['DELETE'].disabled = True
This code leverages the Field.disabled property added in Django 1.9. As the documentation says, "even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data," so you don't need to add more code to prevent deletion.
In your inline, you can add the flag can_delete=False
EG:
class MyInline(admin.TabularInline):
model = models.mymodel
can_delete = False
I found a very easy solution to quietly avoid unwanted deletion of some inlines. You can just override delete_forms property method.
This works not just on admin, but on regular inlines too.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
#property
def deleted_forms(self):
deleted_forms = super(MyInlineFormSet, self).deleted_forms
for i, form in enumerate(deleted_forms):
# Use form.instance to access object instance if needed
if some_criteria_to_prevent_deletion:
deleted_forms.pop(i)
return deleted_forms
I import
from django.contrib.sites.models import Site in models.py file.
In have this following in admin.py file:
class SitesAdmin(admin.ModelAdmin):
pass
admin.site.unregister(Site)
admin.site.register(Site, SitesAdmin)**
I want to attach validation to the site.domain field in admin.py, How i can accomplish this? please help.
First, specifying an empty ModelAdmin class is unnecessary, the following will work if you don't need to customize the admin:
admin.site.register(Site) # Notice that no ModelAdmin is passed
Now, to your question. You have to create a custom form. Then, you override the clean_domain method of the ModelForm. You can validate any field with the method(s) clean_FOO, where FOO is the field name.
from django import forms
class SiteAdminForm(forms.ModelForm):
def clean_domain(self):
domain = self.cleaned_data.get('domain')
# Custom validation here
return domain
class SiteAdmin(admin.ModelAdmin):
form = SiteAdminForm
admin.site.unregister(Site)
admin.site.register(Site, SiteAdmin)