Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for the field DateTimeRangeField from postgres.fields #950

Open
omargawdat opened this issue Jan 8, 2025 · 3 comments
Labels
enhancement New feature or request

Comments

@omargawdat
Copy link
Contributor

omargawdat commented Jan 8, 2025

Suggestion:

Wouldn't it be nice to add support for the field DateTimeRangeField from Postgres as this field is used a lot.

Screenshot

image

Code

from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.fields import DateTimeRangeField, RangeOperators
from django.db import models
from django.db.models import Q

class Event(models.Model):
    title = models.CharField(max_length=200)
    timespan = DateTimeRangeField(null=True)
    capacity = models.PositiveIntegerField()
    cancelled = models.BooleanField(default=False)

    class Meta:
        constraints = [
            ExclusionConstraint(
                name="prevent_overlapping_events",
                expressions=[
                    ("timespan", RangeOperators.OVERLAPS),
                ],
                condition=Q(cancelled=False),
            ),
            models.CheckConstraint(
                check=Q(capacity__gte=1),
                name="capacity_gte_1"
            ),
        ]

Documentation URL

https://docs.djangoproject.com/en/5.1/ref/contrib/postgres/fields/#daterangefield

@omargawdat
Copy link
Contributor Author

i have implemented a custom Widget to handle it, and it works fine for me:

image
from django import forms
from django.contrib import admin
from django.contrib.postgres.fields import DateTimeRangeField
from django.db.backends.postgresql.psycopg_any import DateTimeTZRange
from django.forms.widgets import MultiWidget
from unfold.admin import ModelAdmin
from unfold.widgets import UnfoldAdminSplitDateTimeWidget

from apps.schedule.models.test_model import Event


class UnfoldAdminDateTimeRangeWidget(MultiWidget):
    def __init__(self, attrs=None, error_messages=None):
        widgets = [
            UnfoldAdminSplitDateTimeWidget(attrs={'placeholder': 'Start date/time'}),
            UnfoldAdminSplitDateTimeWidget(attrs={'placeholder': 'End date/time'}),
        ]
        super().__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            return [value.lower, value.upper]
        return [None, None]

    def value_from_datadict(self, data, files, name):
        value_0 = self.widgets[0].value_from_datadict(data, files, f'{name}_0')
        value_1 = self.widgets[1].value_from_datadict(data, files, f'{name}_1')

        # Improved error handling for partial dates
        if bool(value_0) ^ bool(value_1):  # XOR to check if only one value is provided
            if value_0:
                raise forms.ValidationError("End date is required when start date is provided")
            else:
                raise forms.ValidationError("Start date is required when end date is provided")

        if value_0 and value_1:
            return [value_0, value_1]
        return None


class UnfoldDateTimeRangeField(forms.MultiValueField):
    widget = UnfoldAdminDateTimeRangeWidget

    def __init__(self, **kwargs):
        kwargs.pop('default_bounds', None)
        required = kwargs.get('required', True)
        fields = (
            forms.SplitDateTimeField(required=required),
            forms.SplitDateTimeField(required=required),
        )
        super().__init__(
            fields=fields,
            require_all_fields=required,
            **kwargs
        )

    def compress(self, values):
        if not values:
            return None

        start_date, end_date = values

        # Check if only one date is provided
        if bool(start_date) ^ bool(end_date):
            if start_date:
                raise forms.ValidationError("End date is required when start date is provided")
            else:
                raise forms.ValidationError("Start date is required when end date is provided")

        if not any(values):
            return None

        try:
            if start_date and end_date and start_date > end_date:
                raise forms.ValidationError(
                    "Invalid date range - start date cannot be after end date"
                )
            return DateTimeTZRange(lower=start_date, upper=end_date)
        except TypeError:
            return None
        except ValueError as e:
            raise forms.ValidationError(str(e))


@admin.register(Event)
class EventAdmin(ModelAdmin):
    formfield_overrides = {
        DateTimeRangeField: {'form_class': UnfoldDateTimeRangeField},
    }

@lukasvinclav
Copy link
Contributor

Please create PR and I will take a look. Thanks.

@omargawdat
Copy link
Contributor Author

Please check the PR #954

@lukasvinclav lukasvinclav changed the title Feat: add support for the field DateTimeRangeField from postgres.fields feat: add support for the field DateTimeRangeField from postgres.fields Jan 14, 2025
@lukasvinclav lukasvinclav added the enhancement New feature or request label Jan 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants