Skip to content

Commit

Permalink
Merge pull request #1093 from centerofci/split-datetime-into-three-ui…
Browse files Browse the repository at this point in the history
…-types

Split datetime into three UI types
  • Loading branch information
silentninja committed Mar 8, 2022
2 parents 67e615a + 837550f commit 10fa641
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 138 deletions.
9 changes: 9 additions & 0 deletions db/functions/hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ def _is_hint_applicable_to_types(hint):
email = _make_hint("email")


duration = _make_hint("duration")


time = _make_hint("time")


date = _make_hint("date")


literal = _make_hint("literal")


Expand Down
35 changes: 29 additions & 6 deletions db/types/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,23 +183,46 @@ def _add_to_db_type_hintsets(db_types, hints):
hints_for_numeric_db_types = (hints.comparable,)
_add_to_db_type_hintsets(numeric_db_types, hints_for_numeric_db_types)

# point in time db types get the "point_in_time" hint
point_in_time_db_types = (
PostgresType.DATE,
# time of day db types get the "time" hint
time_of_day_db_types = (
PostgresType.TIME,
PostgresType.TIME_WITH_TIME_ZONE,
PostgresType.TIME_WITHOUT_TIME_ZONE,
)
_add_to_db_type_hintsets(time_of_day_db_types, (hints.time,))

# point in time db types get the "point_in_time" hint
point_in_time_db_types = (
*time_of_day_db_types,
PostgresType.DATE,
)
hints_for_point_in_time_types = (hints.point_in_time,)
_add_to_db_type_hintsets(point_in_time_db_types, hints_for_point_in_time_types)

# date db types get the "date" hint
date_db_types = (
PostgresType.DATE,
)
_add_to_db_type_hintsets(date_db_types, (hints.date,))

# datetime db types get the "date" and "time" hints
datetime_db_types = (
PostgresType.TIMESTAMP,
PostgresType.TIMESTAMP_WITH_TIME_ZONE,
PostgresType.TIMESTAMP_WITHOUT_TIME_ZONE,
)
hints_for_point_in_time_types = (hints.point_in_time,)
_add_to_db_type_hintsets(point_in_time_db_types, hints_for_point_in_time_types)
_add_to_db_type_hintsets(datetime_db_types, (hints.date, hints.time,))

# duration db types get the "duration" hints
duration_db_types = (
PostgresType.INTERVAL,
)
_add_to_db_type_hintsets(duration_db_types, (hints.duration,))

# time related types get the "comparable" hint
time_related_db_types = (
*point_in_time_db_types,
PostgresType.INTERVAL,
*duration_db_types,
)
hints_for_time_related_types = (hints.comparable,)
_add_to_db_type_hintsets(time_related_db_types, hints_for_time_related_types)
Expand Down
8 changes: 8 additions & 0 deletions mathesar/api/display_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
{
"options": [{"name": "format", "type": "string"}]
},
MathesarTypeIdentifier.TIME.value:
{
"options": [{"name": "format", "type": "string"}]
},
MathesarTypeIdentifier.DATE.value:
{
"options": [{"name": "format", "type": "string"}]
},
MathesarTypeIdentifier.DURATION.value:
{
"options": [{"name": "format", "type": "string"}]
Expand Down
131 changes: 10 additions & 121 deletions mathesar/api/serializers/shared_serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from abc import ABC, abstractmethod

import arrow
from django.core.exceptions import ImproperlyConfigured
from rest_framework import serializers

Expand Down Expand Up @@ -102,7 +99,7 @@ class CustomBooleanLabelSerializer(MathesarErrorMessageMixin, serializers.Serial
FALSE = serializers.CharField()


DISPLAY_OPTIONS_SERIALIZER_MAPPING_KEY = 'mathesar_type'
DISPLAY_OPTIONS_SERIALIZER_MAPPING_KEY = 'db_type'


class BooleanDisplayOptionSerializer(MathesarErrorMessageMixin, OverrideRootPartialMixin, serializers.Serializer):
Expand All @@ -115,118 +112,16 @@ class NumberDisplayOptionSerializer(MathesarErrorMessageMixin, OverrideRootParti
locale = serializers.CharField(required=False)


class AbstractDateTimeFormatValidator(ABC):
requires_context = True

def __init__(self):
pass

def __call__(self, value, serializer_field):
self.date_format_validator(value, serializer_field)

def date_format_validator(self, value, serializer_field):
try:
timestamp_with_tz_obj = arrow.get('2013-09-30T15:34:00.000-07:00')
parsed_datetime_str = timestamp_with_tz_obj.format(value)
datetime_object = arrow.get(parsed_datetime_str, value)
except ValueError:
raise serializers.ValidationError(f"{value} is not a valid format used for parsing a datetime.")
else:
self.validate(datetime_object, value, serializer_field)

@abstractmethod
def validate(self, datetime_obj, display_format, serializer_field):
pass


class TimestampWithTimeZoneFormatValidator(AbstractDateTimeFormatValidator):

def validate(self, datetime_obj, display_format, serializer_field):
pass


class TimestampWithoutTimeZoneFormatValidator(AbstractDateTimeFormatValidator):

def validate(self, datetime_obj, display_format, serializer_field):
if 'z' in display_format.lower():
raise serializers.ValidationError(
"Timestamp without timezone column cannot contain timezone display format"
)


class DateFormatValidator(AbstractDateTimeFormatValidator):

def validate(self, datetime_obj, display_format, serializer_field):
date_obj = arrow.get('2013-09-30')
if datetime_obj.time() != date_obj.time():
raise serializers.ValidationError("Date column cannot contain time or timezone display format")


class TimeWithTimeZoneFormatValidator(AbstractDateTimeFormatValidator):

def validate(self, datetime_obj, display_format, serializer_field):
time_only_format = 'HH:mm:ss.SSSZZ'
time_str = arrow.get('2013-09-30T15:34:00.000-07:00').format(time_only_format)
parsed_time_str = arrow.get(time_str, time_only_format)
if parsed_time_str.date() != datetime_obj.date():
raise serializers.ValidationError("Time column cannot contain date display format")


class TimeWithoutTimeZoneFormatValidator(TimeWithTimeZoneFormatValidator):

def validate(self, datetime_obj, display_format, serializer_field):
if 'z' in display_format.lower():
raise serializers.ValidationError("Time without timezone column cannot contain timezone display format")
return super().validate(datetime_obj, display_format, serializer_field)


class DurationFormatValidator(AbstractDateTimeFormatValidator):

def validate(self, datetime_obj, display_format, serializer_field):
if 'z' in display_format.lower():
raise serializers.ValidationError(
"Duration column cannot contain timezone display format"
)


class DateDisplayOptionSerializer(MathesarErrorMessageMixin, OverrideRootPartialMixin, serializers.Serializer):
format = serializers.CharField(validators=[DateFormatValidator()])


class TimestampWithoutTimezoneDisplayOptionSerializer(
MathesarErrorMessageMixin,
OverrideRootPartialMixin,
serializers.Serializer
):
format = serializers.CharField(validators=[TimestampWithoutTimeZoneFormatValidator()])


class TimestampWithTimezoneDisplayOptionSerializer(
MathesarErrorMessageMixin,
OverrideRootPartialMixin,
serializers.Serializer
):
format = serializers.CharField(validators=[TimestampWithTimeZoneFormatValidator()])


class TimeWithTimezoneDisplayOptionSerializer(
MathesarErrorMessageMixin,
OverrideRootPartialMixin,
serializers.Serializer
):
format = serializers.CharField(validators=[TimeWithTimeZoneFormatValidator()])


class TimeWithoutTimezoneDisplayOptionSerializer(
class TimeFormatDisplayOptionSerializer(
MathesarErrorMessageMixin,
OverrideRootPartialMixin,
serializers.Serializer
):
format = serializers.CharField(validators=[TimeWithoutTimeZoneFormatValidator()])
format = serializers.CharField(max_length=255)


class DurationDisplayOptionSerializer(MathesarErrorMessageMixin, OverrideRootPartialMixin, serializers.Serializer):
format = serializers.CharField(validators=[DurationFormatValidator()])
format = serializers.CharField(max_length=255)


class DisplayOptionsMappingSerializer(
Expand All @@ -237,19 +132,13 @@ class DisplayOptionsMappingSerializer(
serializers_mapping = {
MathesarTypeIdentifier.BOOLEAN.value: BooleanDisplayOptionSerializer,
MathesarTypeIdentifier.NUMBER.value: NumberDisplayOptionSerializer,
('timestamp with time zone',
MathesarTypeIdentifier.DATETIME.value): TimestampWithTimezoneDisplayOptionSerializer,
('timestamp without time zone',
MathesarTypeIdentifier.DATETIME.value): TimestampWithoutTimezoneDisplayOptionSerializer,
('date', MathesarTypeIdentifier.DATETIME.value): DateDisplayOptionSerializer,
('time with time zone', MathesarTypeIdentifier.DATETIME.value): TimeWithTimezoneDisplayOptionSerializer,
('time without time zone', MathesarTypeIdentifier.DATETIME.value): TimeWithoutTimezoneDisplayOptionSerializer,
MathesarTypeIdentifier.DATETIME.value: TimeFormatDisplayOptionSerializer,
MathesarTypeIdentifier.DATE.value: TimeFormatDisplayOptionSerializer,
MathesarTypeIdentifier.TIME.value: TimeFormatDisplayOptionSerializer,
MathesarTypeIdentifier.DURATION.value: DurationDisplayOptionSerializer,
}

def get_mapping_field(self):
mathesar_type = get_mathesar_type_from_db_type(self.context[DISPLAY_OPTIONS_SERIALIZER_MAPPING_KEY])
if mathesar_type == MathesarTypeIdentifier.DATETIME.value:
return self.context[DISPLAY_OPTIONS_SERIALIZER_MAPPING_KEY].lower(), mathesar_type
else:
return mathesar_type
db_type = self.context[DISPLAY_OPTIONS_SERIALIZER_MAPPING_KEY]
mathesar_type = get_mathesar_type_from_db_type(db_type)
return mathesar_type
16 changes: 14 additions & 2 deletions mathesar/database/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
class MathesarTypeIdentifier(Enum):
BOOLEAN = 'boolean'
DATETIME = 'datetime'
TIME = 'time'
DATE = 'date'
DURATION = 'duration'
EMAIL = 'email'
MONEY = 'money'
Expand Down Expand Up @@ -48,12 +50,22 @@ def _get_type_map():
'name': 'Boolean',
'sa_type_names': [PostgresType.BOOLEAN.value]
}, {
'identifier': MathesarTypeIdentifier.DATETIME.value,
'name': 'Date & Time',
'identifier': MathesarTypeIdentifier.DATE.value,
'name': 'Date',
'sa_type_names': [
PostgresType.DATE.value,
]
}, {
'identifier': MathesarTypeIdentifier.TIME.value,
'name': 'Time',
'sa_type_names': [
PostgresType.TIME_WITH_TIME_ZONE.value,
PostgresType.TIME_WITHOUT_TIME_ZONE.value,
]
}, {
'identifier': MathesarTypeIdentifier.DATETIME.value,
'name': 'Date & Time',
'sa_type_names': [
PostgresType.TIMESTAMP_WITH_TIME_ZONE.value,
PostgresType.TIMESTAMP_WITHOUT_TIME_ZONE.value
]
Expand Down
16 changes: 8 additions & 8 deletions mathesar/tests/api/test_column_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,18 +273,18 @@ def test_column_create_display_options(
assert actual_new_col["display_options"] == display_options


_too_long_string = "x" * 256


create_display_options_invalid_test_list = [
("BOOLEAN", {"input": "invalid", "use_custom_columns": False}),
("BOOLEAN", {"input": "checkbox", "use_custom_columns": True, "custom_labels": {"yes": "yes", "1": "no"}}),
("DATE", {'format': 'YYYY-MM-DD hh:mm Z'}),
("DATE", {'format': 'hh:mm Z'}),
("NUMERIC", {"show_as_percentage": "wrong value type"}),
("TIMESTAMP WITH TIME ZONE", {'format': 'xyz'}),
("TIMESTAMP WITHOUT TIME ZONE", {'format': 'xyz'}),
("TIMESTAMP WITHOUT TIME ZONE", {'format': 'YYYY-MM-DD hh:mm Z'}),
("TIME WITH TIME ZONE", {'format': 'YYYY-MM-DD hh:mm Z'}),
("TIME WITHOUT TIME ZONE", {'format': 'YYYY-MM-DD hh:mm'}),
("TIME WITHOUT TIME ZONE", {'format': 'hh:mm Z'}),
("DATE", {'format': _too_long_string}),
("TIMESTAMP WITH TIME ZONE", {'format': []}),
("TIMESTAMP WITHOUT TIME ZONE", {'format': _too_long_string}),
("TIME WITH TIME ZONE", {'format': _too_long_string}),
("TIME WITHOUT TIME ZONE", {'format': {}}),
]


Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
alembic==1.6.5
arrow==1.2.1
charset-normalizer==2.0.7
clevercsv==0.6.8
Django==3.1.14
Expand Down

0 comments on commit 10fa641

Please sign in to comment.