Change the default widget for date fields in Django admin - django

How can I change the default widget for DateField instances in Django ADMIN?
I am aware of how to do this for a ModelForm - How do you change the default widget for all Django date fields in a ModelForm? - by providing formfield_callback that checks the field type and then provides a form field with the new widget.
However, some of my ModelAdmin's may already have a custom ModelForm, some haven't. I don't want to provide/change a ModelForm wherever there's a date field (that's the point).
A custom ModelAdmin which could be used for subclassing would be ok, though. How can a ModelAdmin change the date fields of its form, independent of whether a custom ModelForm has been provided or not?
Maybe a ModelAdmin with get_form() calling its super then setting the form's formfield_callback? Seems a little convoluted, not sure about the timing, and also then there's three things to be aware of if any ModelAdmin needs further customization...
P.S.: I'm asking this after https://stackoverflow.com/questions/8484811/jquery-datepicker-in-django-admin was erroneously closed for being an "exact duplicate" of the above-mentioned ModelForm question. I hope the difference is now clear.
Btw my personal motivation is the year navigation of django admin's default datepicker. Really bad for birthdays... :-) yes there's a three-year old ticket https://code.djangoproject.com/ticket/9388

I was able to customize the default date widget in the Django admin. I followed two tutorials (from Simple is Better than Complex and Django Custom Widget)
Write a custom widget in a file called widgets.py inside your Django app
from django.forms import DateInput
class CustomDatePickerInput(DateInput):
template_name = "app_name/widgets/custom_datepicker.html"
class Media:
css = {
"all": (
"https://cdnjs.cloudflare.com/ajax/libs/datepicker/0.6.5/datepicker.min.css",
)
}
js = (
"https://code.jquery.com/jquery-3.4.1.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/datepicker/0.6.5/datepicker.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/datepicker/0.6.5/i18n/datepicker.es-ES.min.js",
)
Notice that the CSS and JS files that you may need should be added in the internal Media class as shown in the code above. In this case it is adding styles for the custom date picker.
Define the template for your widget, for more date picker's options see here, put this file in app_name/templates/app_name/widgets/custom_datepicker.html or make sure that this template matches the value of template_name in your custom widget:
{% load i18n %}
{% include "django/forms/widgets/input.html" %}
{% get_current_language as LANGUAGE_CODE %}
<script>
$(function () {
const DATE_PICKER_OPTIONS = {
'en-us': {
format: 'yyyy-mm-dd',
language: ''
},
'es': {
format: 'dd/mm/yyyy',
language: 'es-ES'
}
};
let options = DATE_PICKER_OPTIONS['{{ LANGUAGE_CODE }}'];
$("input[name='{{ widget.name }}']").datepicker({
format: options.format,
language: options.language
});
});
</script>
You need to indicate what widget to use in date field when registering your model in the Django admin (use formfield_overrides)
#admin.register(YourModel)
class YourModelAdmin(admin.ModelAdmin):
formfield_overrides = {models.DateField: {"widget": CustomDatePickerInput}}
Once this is completed, you will have a custom widget for dates in the Django admin. Keep in mind that this is just a guide, you can go further by reviewing the two tutorials I indicated.
Note: the implementation of this widget is also supporting internationalization.

ModelAdmin can only change the widget of the form it renders, so it would be difficult to override the widgets of a custom form passed to it - at least I can't think of a clean way to do it.
I can see a few options for you, depending on how/what you want to customize.
Modify the javascript that does the magic for the widget. This seems easy enough if your change can be handled there. Simply provide your own js/calendar.js and js/admin/DateTimeShortcuts.js the same way you would override the admin's templates. This has the added bonus of being upgrade friendly; since you are not modifying the django code base; also the change would be immediate for all forms that use the widget.
Customize the built-in django widget to do what you want. This has the benefit of instantly working without any modifications of your existing code, but is not upgrade friendly unless you are careful about patching sources. The source is at django.contrib.admin.widgets
Provide your own custom widget - but then you are back to the problem of going through the code and manually overriding all widget instances with your custom one.
Use something like grappelli and modify the templates/js to do what you want. Added bonus here is that you'll get lots of other functionality for free (like the ability to have a dashboard, if you plug it in with django-admin-tools).
django uses jquery extensively in their admin and they have their own jquery namespace (django.jQuery); so if you plan on doing some custom widgets make sure you use the right namespace to avoid strange problems.

Related

How to display a custom form with multiple widgets for a field in Django admin?

The Django docs say you can add a form to the admin UI:
class ArticleAdmin(admin.ModelAdmin):
form = MyArticleAdminForm
I want a custom editing UI for a special field in my model, where I display multiple widgets. (It's not exactly the same, but an analogy might be an old-school hex editor widget, where you want fine editing control on a big blob of information.) Perhaps I could break the multiple values into multiple database objects and use an InlineAdmin, but I have app-specific reasons to not do that.
I thought I'd use a Form object with some custom fields, but Django says it must be a ModelForm:
<class 'myapp.admin.MyAdmin'>: (admin.E016) The value of 'form' must inherit from 'BaseModelForm'.
Is it possible to display multiple widgets (basically a very custom form) for a single model value in Django admin?
EDIT: It looks like MultiWidget might work? I'm gonna look into that. Also, this question is related. That suggests I should just change the widget on the field.
The answer was to make a MultiWidget, overriding:
__init__ to set up the widgets
decompress and value_from_datadict to unpack and pack the field value
template_name to render my own template
get_context to make the context for the template

In Django Forms, how to add a filter to a SelectedMultiple control?

I'm working with Django Forms. In my model, i have a ManyToMany relationship between class X and class Y and Django shows a very annoying MultipleChoice control to edit this relationship. I would like to add a filter so editing the X object the user can filter the Y objects by name while he writes the name to finally select them
Some idea about how to do this in Django?
By default, a ManyToManyField in a Django Model will be represented by a ModelMultipleChoiceField in the ModelForm, which itself uses a SelectMultiple widget. This widget uses the default browser <select multiple="multiple"> element, which results in your "annoying" multiple choice control.
So in order to replace it, you should override the ModelMultipleChoiceField in your form to pass it your own widget (which would subclass SelectMultiple and override the template used):
my_field = forms.ModelMultipleChoiceField(queryset=Y.objects.all(), widget=MySelectMultiple)
However, many people have already done this kind of thing, so it's probably easier to use a package that has a nice multiple choice widget to your liking.
A very popular jquery module on the front-end is select2. If you want to use it, there are some django packages that already support it, popular ones are django-autocomplete-light and django-select2

How to add a custom button or link beside a form field

I am using Django with crispy_forms third party library. I want to add a link beside a form field like some forms in Django admin app. How can I do this?
You've picked quite a complicated example that uses a method I wouldn't even recommend. But I'll try to explain how you see what you're seeing, and keep it short.
That is a AdminTimeWidget(forms.TimeInput) and a AdminDateWidget both nested in a AdminSplitDateTime(SplitDateTimeWidget(MultiWidget)). The MultiWidget part of this isn't really important, that's just how you bind two widgets together to provide one value (a datetime.datetime).
Here's what AdminTimeWidget looks like:
class AdminTimeWidget(forms.TimeInput):
#property
def media(self):
extra = '' if settings.DEBUG else '.min'
js = [
'vendor/jquery/jquery%s.js' % extra,
'jquery.init.js',
'calendar.js',
'admin/DateTimeShortcuts.js',
]
return forms.Media(js=["admin/js/%s" % path for path in js])
def __init__(self, attrs=None, format=None):
final_attrs = {'class': 'vTimeField', 'size': '8'}
if attrs is not None:
final_attrs.update(attrs)
super().__init__(attrs=final_attrs, format=format)
That adds a DateTimeShortcuts.js script to the page (in the way that Admin Widgets can, via the form media property) and it's that script that iterates input tags looking for date and time inputs.
There's a LOT of machinery involved to get that happening but again, in effect, it's just a bit of javascript that looks for a date/time input and adds the HTML client-side.
But you probably don't want to do that.
As I said, that's a very complicated widget, and in Admin where it's harder to alter things on the fly. If you want to write an Admin widget, you probably do want to go that way.
But if you already control the template, or a crispy layout, you could just bung in some HTML. Crispy has an HTML element that you can throw into layouts. This is well documented.
Or if you want a reusable widget, you could use a custom template. Since Django 1.11, Widgets use templates to render.
Create a widget, borrowing from an existing one to save time
from django.forms import widgets
class DateWithButtonWidget(widgets.DateInput):
template_name = 'widgets/date_with_button.html'
Customise the template with the HTML you want:
{% include "django/forms/widgets/input.html" %} <button>MY BUTTON</button>
Use that widget in your form:
class MyForm(forms.ModelForm):
fancydate = forms.DateField(widget=DateWithButtonWidget)
Of course, wiring that button to do something is all up to you. Using a fully-scripted option might be what you need after all.

Specifying specific form format in Django

I am using materializecss to give my django site some material elements. I have put together a form (the 'old' way using html) but now realised I need to use a django form instead. The problem is, these forms don't play well with materialises built in column system (they use classes to determine rows and column spacing). Here is an example of the layout I set up so far. However when defining the form through form.py, it spits out one input per layer.
My question is: what can I do to either a) get django to work with the html-defined form or b) make a 'form template' to give the input fields the appropriate classes?
If you want to see the code I can post some but I'm quite a new coder so it's messy.
Thanks!
There are three ways I can think of off the top of my head.
If you want full control over the HTML form, in a Django template or HTML form, simply map the names of your fields to match the underlying field names in the Django form. This way, when POSTed back to your view, Django will automatically link up the POSTed fields with the Django form fields.
For example, if you have a field username in your Django form (or Django model if using ModelForm), you could have an element <input type="text" name="username" maxlength="40"> (that you can style any way you need) on your HTML form that Django will happily parse into your Django form field, assuming your view is plumbed correctly. There is an example of this method in the Django documentation.
Another way is to customize the Django form field widgets in your Django form definition. The Django documentation talks a little bit about how to do this. This is great for one offs, but is probably not the best approach if you expect to reuse widgets.
The final approach would be to subclass Django form field widgets to automatically provide whatever attributes you need. For example, we use Bootstrap and have subclassed nearly all of the widgets we use to take advantage of Bootstrap classes.
class BootstrapTextInput(forms.TextInput):
def __init__(self, attrs=None):
final_attrs = {'class': 'form-control'}
if attrs is not None:
final_attrs.update(attrs)
super().__init__(attrs=final_attrs)
Then it's simply a matter of letting the Django form know which widget to use for your form field.
class UsernameForm(forms.ModelForm):
class Meta:
model = auth.get_user_model()
fields = ['username']
widgets = {'username': BootstrapTextInput()}
Hope this helps. Cheers!

Different ManyToManyField widget

The default django 1.0.2 ManyToManyField widget (a multi-select) is difficult to use when there are a lot of things in the select box. Is there another widget available that gives a comma separated list of id's in a textarea? If this is not available what do I need to do to write one, and have it show up on ModelForm.as_p() and in the admin site?
If there are no existing widgets that do what you want (and I don't think there are) then you'll need to write your own. Unfortunately, the Django documentation doesn't show you how to do this, but it's not hard to figure out by looking at the source-code forms/widgets.py copying an existing widget and modifying it.
I believe setting raw_id_fields on the manytomanyfield actually outputs a TextInput widget with a comma-separated list of ids.
You may just override this in admin.py, in the corresponding ModelForm and force a TextArea widget on it.
In the Admin you can use filter horizontal and/or filter vertical:
class MyModelAdmin(admin.ModelAdmin):
filter_horizontal = ['many_to_many_field_name']
filter_horizontal = ['another_many_to_many_field_name']
doc