Repository: mathesar-foundation/mathesar

Issue:
Implement display options for Duration type
## Problem
The Duration Mathesar type supports the following display options:
- Min unit
- Max unit
- Whether to show unit labels

We will combine this into a single `format` string.

## Solution
(1) We should store these display options in the following format in the `display_options` field of the corresponding column.
```
{
  "format": "HH:mm:ss.SSS"
}
```

(2) We should also validate these so that:
- Only columns of this type can have these display options. They should not be able to be set if the column is of a different type.
- `format` should be a valid JavaScript duration format.

(3) If the column type is changed, the display options should be deleted.

(4) We should add supported display options to the `types` endpoint.

## Additional Context
- [Design of Duration type options on Figma](https://www.figma.com/proto/Uaf1ntcldzK2U41Jhw6vS2/Mathesar-MVP?page-id=4260%3A37440&node-id=4270%3A41231&viewport=324%2C48%2C0.29&scaling=contain&starting-point-node-id=4270%3A41231&show-proto-sidebar=1)
- Blocked by #658
- #392 


Discussion:

Resolving patch:
diff --git a/mathesar/api/display_options.py b/mathesar/api/display_options.py
--- a/mathesar/api/display_options.py
+++ b/mathesar/api/display_options.py
@@ -16,6 +16,10 @@
                         {"name": "locale", "type": "string"}]
         },
     MathesarTypeIdentifier.DATETIME.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
--- a/mathesar/api/serializers/shared_serializers.py
+++ b/mathesar/api/serializers/shared_serializers.py
@@ -165,7 +165,7 @@ def validate(self, datetime_obj, display_format, serializer_field):
 class TimeWithTimeZoneFormatValidator(AbstractDateTimeFormatValidator):
 
     def validate(self, datetime_obj, display_format, serializer_field):
-        time_only_format = 'HH:mm:ssZZ'
+        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():
@@ -180,6 +180,15 @@ def validate(self, datetime_obj, display_format, serializer_field):
         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()])
 
@@ -216,6 +225,10 @@ class TimeWithoutTimezoneDisplayOptionSerializer(
     format = serializers.CharField(validators=[TimeWithoutTimeZoneFormatValidator()])
 
 
+class DurationDisplayOptionSerializer(MathesarErrorMessageMixin, OverrideRootPartialMixin, serializers.Serializer):
+    format = serializers.CharField(validators=[DurationFormatValidator()])
+
+
 class DisplayOptionsMappingSerializer(
     MathesarErrorMessageMixin,
     ReadWritePolymorphicSerializerMappingMixin,
@@ -231,6 +244,7 @@ class DisplayOptionsMappingSerializer(
         ('date', MathesarTypeIdentifier.DATETIME.value): DateDisplayOptionSerializer,
         ('time with time zone', MathesarTypeIdentifier.DATETIME.value): TimeWithTimezoneDisplayOptionSerializer,
         ('time without time zone', MathesarTypeIdentifier.DATETIME.value): TimeWithoutTimezoneDisplayOptionSerializer,
+        MathesarTypeIdentifier.DURATION.value: DurationDisplayOptionSerializer,
     }
 
     def get_mapping_field(self):


Test patch:
diff --git a/mathesar/tests/api/test_column_api.py b/mathesar/tests/api/test_column_api.py
--- a/mathesar/tests/api/test_column_api.py
+++ b/mathesar/tests/api/test_column_api.py
@@ -238,6 +238,7 @@ def test_column_create_invalid_default(column_test_table, client):
     ("BOOLEAN", {"input": "dropdown"}),
     ("BOOLEAN", {"input": "checkbox", "custom_labels": {"TRUE": "yes", "FALSE": "no"}}),
     ("DATE", {'format': 'YYYY-MM-DD'}),
+    ("INTERVAL", {'format': 'DD HH:mm:ss.SSS'}),
     ("NUMERIC", {"show_as_percentage": True}),
     ("NUMERIC", {"show_as_percentage": True, "locale": "en_US"}),
     ("TIMESTAMP WITH TIME ZONE", {'format': 'YYYY-MM-DD hh:mm'}),