How to use work week in DateField in Django admin - django

In my model, I say:
class Foo(models.Model):
start = models.DateField(help_text='Start Date')
In the django admin, when adding a new Foo object, I see a text field with a calendar attached allowing me to select the date. I want to customize this a little bit so that I can either select the date from the calendar or enter something like WW12'22 in the textfield and during Save, this gets converted into a DateField. I am not sure how to do implement this in the django admin. Any ideas would be helpful.
I try something like this, but I get an invalid date when I enter '05 22' (i.e, WW05'22).
class FooForm(forms.ModelForm):
start_date = forms.DateField(widget=forms.DateInput(format='%W %y'), localize=True)

You could create your own DateField subclass and when the provided value isn't parsed as a date you implement your own parsing.
Since you need to provide the day of the week to use week number when parsing, replace the "WW" part of your input with 1 and parse this as the day of the week (1 is Monday)
admin.py
import re
import datetime
from django.contrib import admin
from django import forms
from .models import Foo
from django.contrib.admin.widgets import AdminDateWidget
class MyDateField(forms.DateField):
def to_python(self, value):
try:
return super().to_python(value)
except forms.ValidationError:
if re.match(r'WW\d+\'\d+', value):
return datetime.datetime.strptime(value.replace('WW', '1-'), '%w-%W\'%y')
raise
class FooForm(forms.ModelForm):
start_date = MyDateField(widget = AdminDateWidget)
class Meta:
model = Foo
fields = '__all__'
class FooAdmin(admin.ModelAdmin):
form = FooForm
admin.site.register(Foo, FooAdmin)

Related

How to get Django ModelForm to accept a date in a particular format (or convert to an acceptable fomat)?

I have a model with a datefield. I am using this model to create a form using the ModelForm class.
The problem is that the user can enter the date in multiple formats (d-m-yy, dd-mm-yy, dd-mm-yyyy) (not thinking about locales for now).
After the form is submitted. Django form.is_valid() fails because of the date format.
I need to handle different formats but can't figure out how to do that. Here is a minimal Form and Model class: (assume correct imports)
class MyForm(forms.ModelForm):
class Meta:
model = models.MyDateModel
fields = ["date"]
widgets = {
"date": forms.DateInput(),
}
Model Class
class MyDateModel(models.Model):
date = models.DateField()
The easiest way is here.
Or you can add clean_date function to your form class, check date input using regex and convert it to Django datetime format. Something like this:
import datetime, re
class MyForm(forms.ModelForm):
...
def clean_date(date):
date = self.cleaned_data.get('date')
if re.match('^\d{1,2}-\d{1,2}-\d{2}', date) is not None:
return datetime.datetime.strptime(date, "%d-%m-%y").strftime("%Y-%m-%d")
if re.match('^\d{2}-\d{2}-\d{4}', date) is not None:
return datetime.datetime.strptime(date, "%d-%m-%Y").strftime("%Y-%m-%d")

Reload choices dynamically when using MultipleChoiceFilter

I am trying to construct a MultipleChoiceFilter where the choices are the set of possible dates that exist on a related model (DatedResource).
Here is what I am working with so far...
resource_date = filters.MultipleChoiceFilter(
field_name='dated_resource__date',
choices=[
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
When this is displayed in a html view...
This works fine at first, however if I create new DatedResource objects with new distinct date values I need to re-launch my webserver in order for them to get picked up as a valid choice in this filter. I believe this is because the choices list is evaluated once when the webserver starts up, not every time my page loads.
Is there any way to get around this? Maybe through some creative use of a ModelMultipleChoiceFilter?
Thanks!
Edit:
I tried some simple ModelMultipleChoice usage, but hitting some issues.
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
queryset=resource_models.DatedResource.objects.all().values_list('date', flat=True).order_by('date').distinct(),
label="Resource Date"
)
The HTML form is showing up just fine, however the choices are not accepted values to the filter. I get "2019-04-03" is not a valid value. validation errors, I am assuming because this filter is expecting datetime.date objects. I thought about using the coerce parameter, however those are not accepted in ModelMultipleChoice filters.
Per dirkgroten's comment, I tried to use what was suggested in the linked question. This ends up being something like
resource_date = filters.ModelMultipleChoiceFilter(
field_name='dated_resource__date',
to_field_name='date',
queryset=resource_models.DatedResource.objects.all(),
label="Resource Date"
)
This also isnt what I want, as the HTML now form is now a) displaying the str representation of each DatedResource, instead of the DatedResource.date field and b) they are not unique (ex if I have two DatedResource objects with the same date, both of their str representations appear in the list. This also isnt sustainable because I have 200k+ DatedResources, and the page hangs when attempting to load them all (as compared to the values_list filter, which is able to pull all distinct dates out in seconds.
One of the easy solutions will be overriding the __init__() method of the filterset class.
from django_filters import filters, filterset
class FooFilter(filterset.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
self.filters['user'].extra['choices'] = [(d, d.strftime('%Y-%m-%d')) for d in sorted(
resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())]
except (KeyError, AttributeError):
pass
resource_date = filters.MultipleChoiceFilter(field_name='dated_resource__date', choices=[], label="Resource Date")
NOTE: provide choices=[] in your field definition of filterset class
Results
I tested and verified this solution with following dependencies
1. Python 3.6
2. Django 2.1
3. DRF 3.8.2
4. django-filter 2.0.0
I used following code to reproduce the behaviour
# models.py
from django.db import models
class Musician(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return f'{self.name}'
class Album(models.Model):
artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
release_date = models.DateField()
def __str__(self):
return f'{self.name} : {self.artist}'
# serializers.py
from rest_framework import serializers
class AlbumSerializer(serializers.ModelSerializer):
artist = serializers.StringRelatedField()
class Meta:
fields = '__all__'
model = Album
# filters.py
from django_filters import rest_framework as filters
class AlbumFilter(filters.FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters['release_date'].extra['choices'] = self.get_album_filter_choices()
def get_album_filter_choices(self):
release_date_list = Album.objects.values_list('release_date', flat=True).distinct()
return [(date, date) for date in release_date_list]
release_date = filters.MultipleChoiceFilter(choices=[])
class Meta:
model = Album
fields = ('release_date',)
# views.py
from rest_framework.viewsets import ModelViewSet
from django_filters import rest_framework as filters
class AlbumViewset(ModelViewSet):
serializer_class = AlbumSerializer
queryset = Album.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_class = AlbumFilter
Here I've used the django-filter with DRF.
Now, I populated some data through Django Admin console. After that, the album api become as below,
and I got the release_date as
Then, I added new entry through Django admin -- (Screenshot) and I refresh the DRF API endpoint and the possible choices became as below,
I have looked into your problem and I have following suggestions
The Problem
You have got the problem right. Choices for your MultipleChoiceFilter are calculated statically whenever you run server.Thats why they don't get updated dynamically whenever you insert new instance in DatedResource.
To get it working correctly, you have to provide choices dynamically to MultipleChoiceFilter. I searched in documentation but did not find anything regarding this. So here is my solution.
The solution
You have to extend MultipleChoiceFilter and create your own filter class. I have created this and here it is.
from typing import Callable
from django_filters.conf import settings
import django_filters
class LazyMultipleChoiceFilter(django_filters.MultipleChoiceFilter):
def get_field_choices(self):
choices = self.extra.get('choices', [])
if isinstance(choices, Callable):
choices = choices()
return choices
#property
def field(self):
if not hasattr(self, '_field'):
field_kwargs = self.extra.copy()
if settings.DISABLE_HELP_TEXT:
field_kwargs.pop('help_text', None)
field_kwargs.update(choices=self.get_field_choices())
self._field = self.field_class(label=self.label, **field_kwargs)
return self._field
Now you can use this class as replacement and pass choices as lambda function like this.
resource_date = LazyMultipleChoiceFilter(
field_name='dated_resource__date',
choices=lambda: [
(d, d.strftime('%Y-%m-%d')) for d in
sorted(resource_models.DatedResource.objects.all().values_list('date', flat=True).distinct())
],
label="Resource Date"
)
Whenever instance of filter will be created choices will be updated dynamically. You can also pass choices statically (without lambda function) to this field if want default behavior.

How can I define the date format in models in django?

I'm defining a date variable inside my model AccessRecord using DateField attribute and I've tried every possible thing to define it. But every time a new error arises. Sometimes it says "Date is not defined" sometimes "Date should be in YYYY-MM-DD format" during migration.
Can someone give me a permanent solution for this? I'm using django 2.1.7.
I've tried default='' and default=blank and quite of few others
from django.db import models
from datetime import date
class AccessRecord(models.Model):
name = models.ForeignKey(Webpage, on_delete=models.PROTECT)
date = models.DateField(_("Date"), default=date.today)
def __str__(self):
return str(self.date)
This should be sufficent
from django.db import models
class AccessRecord(models.Model):
name = models.ForeignKey(Webpage, on_delete=models.PROTECT)
date = models.DateField(auto_now_add=True))
def __str__(self):
return str(self.date)
And for the formatting stuff use the #property mentioned in stackoverflow.com/q/20229198/4107823 as commented by PetarP

In Django how can I get a widget that accepts an ISO 8601 datetime string and works with a DateTimeField?

I want to submit an ISO 8601 datetime string with a time zone designator to a django form that I am writing. I want to use a standard DateTimeField for the datetime but have a widget that accepts such a datetime string. Is this possible, or am I confused about the relationship between Widgets and Fields? How do I do it?
Parsing the string is easy, using django.utils.datepase.parse_datetime.
I have tried writing a widget like so:
from django.forms import HiddenInput
from django.utils.dateparse import parse_datetime
class DateTimeInputForMachines(HiddenInput):
def _format_value(self, value):
out = super(DateTimeInputForMachines, self)._format_value(parse_datetime(value))
return out
and using it in a ModelForm like so:
from mywidgets import DateTimeInputForMachines
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['start', 'thing']
widgets = {
'start': DateTimeInputForMachines()
}
but the 'start' field fails validation with "Enter a valid date/time."
Widgets are for displaying data, and have nothing to do with validation.
If you just need to input a custom date format, there is no need for any subclassing; you can supply input_formats directly on the field (see the documentation).
class MyForm(forms.ModelForm):
start = forms.DateTimeField(input_formats=["%Y-%m-%dT%H:%M:%S.%z"])
class Meta:
model = MyModel
fields = ['start', 'thing']

How to make django-taggit admin text input field a bigger size?

As per the title, how do I tell the admin interface (admin.py) to use a custom size for the tags field of my model which is using django-taggit:
tags = TaggableManager()
Thanks
Similar question to Django Admin - Overriding the widget of a custom form field.
However, you'll need to override any admin.widgets, for example, AdminTextareaWidget, so that the tags are display correctly. Otherwise the form field value will be displayed as
[<TaggedItem: Product object tagged with bar>,
<TaggedItem: Product object tagged with foo>,
<TaggedItem: Product object tagged with baz>]
Sample code to illustrate this.
#forms.py
from django import forms
from django.contrib.admin.widgets import AdminTextareaWidget
from django.utils import six
from models import Product
from taggit.utils import edit_string_for_tags
class TaggitAdminTextareaWidget(AdminTextareaWidget):
# taken from taggit.forms.TagWidget
def render(self, name, value, attrs=None):
if value is not None and not isinstance(value, six.string_types):
value = edit_string_for_tags([o.tag for o in value.select_related("tag")])
return super(TaggitAdminTextareaWidget, self).render(name, value, attrs)
class ProductAdminForm(forms.ModelForm):
class Meta:
model = Product
widgets = {
'tags': TaggitAdminTextareaWidget
}