diff --git a/db/functions/hints.py b/db/functions/hints.py index 33b1b4ddce..b0e941b4de 100644 --- a/db/functions/hints.py +++ b/db/functions/hints.py @@ -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") diff --git a/db/types/base.py b/db/types/base.py index ecea8651e3..08ce5f2a3b 100644 --- a/db/types/base.py +++ b/db/types/base.py @@ -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) diff --git a/mathesar/api/display_options.py b/mathesar/api/display_options.py index a4a4f63bdf..abb7491187 100644 --- a/mathesar/api/display_options.py +++ b/mathesar/api/display_options.py @@ -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"}] diff --git a/mathesar/api/serializers/shared_serializers.py b/mathesar/api/serializers/shared_serializers.py index dfc828bf9e..d25097df17 100644 --- a/mathesar/api/serializers/shared_serializers.py +++ b/mathesar/api/serializers/shared_serializers.py @@ -1,6 +1,3 @@ -from abc import ABC, abstractmethod - -import arrow from django.core.exceptions import ImproperlyConfigured from rest_framework import serializers @@ -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): @@ -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( @@ -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 diff --git a/mathesar/database/types.py b/mathesar/database/types.py index 715b33186d..c54ea17489 100644 --- a/mathesar/database/types.py +++ b/mathesar/database/types.py @@ -13,6 +13,8 @@ class MathesarTypeIdentifier(Enum): BOOLEAN = 'boolean' DATETIME = 'datetime' + TIME = 'time' + DATE = 'date' DURATION = 'duration' EMAIL = 'email' MONEY = 'money' @@ -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 ] diff --git a/mathesar/tests/api/test_column_api.py b/mathesar/tests/api/test_column_api.py index 7d8a20ba48..18a5eb2520 100644 --- a/mathesar/tests/api/test_column_api.py +++ b/mathesar/tests/api/test_column_api.py @@ -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': {}}), ] diff --git a/requirements.txt b/requirements.txt index 22acd8ad96..1290ff8865 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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