Skip to content

Commit

Permalink
Separate naive/aware datetime pickers
Browse files Browse the repository at this point in the history
  • Loading branch information
vidartf committed Feb 4, 2020
1 parent 1349c0e commit d45ac7d
Show file tree
Hide file tree
Showing 6 changed files with 497 additions and 384 deletions.
48 changes: 48 additions & 0 deletions ipywidgets/widgets/trait_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,54 @@ def datetime_from_json(js, manager):
}


def naive_to_json(pydt, manager):
"""Serialize a naive Python datetime object to json.
Instantiating a JavaScript Date object with a string assumes that the
string is a UTC string, while instantiating it with constructor arguments
assumes that it's in local time:
>>> cdate = new Date('2015-05-12')
Mon May 11 2015 20:00:00 GMT-0400 (Eastern Daylight Time)
>>> cdate = new Date(2015, 4, 12) // Months are 0-based indices in JS
Tue May 12 2015 00:00:00 GMT-0400 (Eastern Daylight Time)
Attributes of this dictionary are to be passed to the JavaScript Date
constructor.
"""
if pydt is None:
return None
else:
naivedt = pydt.replace(tzinfo=None)
return dict(
year=naivedt.year,
month=naivedt.month - 1, # Months are 0-based indices in JS
date=naivedt.day,
hours=naivedt.hour, # Hours, Minutes, Seconds and Milliseconds
minutes=naivedt.minute, # are plural in JS
seconds=naivedt.second,
milliseconds=naivedt.microsecond / 1000,
)


def naive_from_json(js, manager):
"""Deserialize a naive Python datetime object from json."""
if js is None:
return None
else:
return dt.datetime(
js["year"],
js["month"] + 1, # Months are 1-based in Python
js["date"],
js["hours"],
js["minutes"],
js["seconds"],
js["milliseconds"] * 1000,
)

naive_serialization = {"from_json": naive_from_json, "to_json": naive_to_json}


def date_to_json(pydate, manager):
"""Serialize a Python date object.
Expand Down
58 changes: 55 additions & 3 deletions ipywidgets/widgets/widget_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from traitlets import Unicode, Bool, validate, TraitError

from .trait_types import datetime_serialization, Datetime
from .trait_types import datetime_serialization, Datetime, naive_serialization
from .valuewidget import ValueWidget
from .widget import register
from .widget_core import CoreWidget
Expand All @@ -20,7 +20,7 @@
@register
class DatetimePicker(DescriptionWidget, ValueWidget, CoreWidget):
"""
Display a widget for picking times.
Display a widget for picking datetimes.
Parameters
----------
Expand All @@ -42,7 +42,7 @@ class DatetimePicker(DescriptionWidget, ValueWidget, CoreWidget):
>>> import datetime
>>> import ipydatetime
>>> datetime_pick = ipydatetime.TimePicker()
>>> datetime_pick = ipydatetime.DatetimePicker()
>>> datetime_pick.value = datetime.datetime(2018, 09, 5, 12, 34, 3)
"""

Expand All @@ -55,10 +55,16 @@ class DatetimePicker(DescriptionWidget, ValueWidget, CoreWidget):
min = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization)
max = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization)

def _validate_tz(self, value):
if value.tzinfo is None:
raise TraitError('%s values needs to be timezone aware' % (self.__class__.__name__,))
return value

@validate("value")
def _validate_value(self, proposal):
"""Cap and floor value"""
value = proposal["value"]
value = self._validate_tz(value)
if self.min and self.min > value:
value = max(value, self.min)
if self.max and self.max < value:
Expand All @@ -69,6 +75,7 @@ def _validate_value(self, proposal):
def _validate_min(self, proposal):
"""Enforce min <= value <= max"""
min = proposal["value"]
min = self._validate_tz(min)
if self.max and min > self.max:
raise TraitError("Setting min > max")
if self.value and min > self.value:
Expand All @@ -79,8 +86,53 @@ def _validate_min(self, proposal):
def _validate_max(self, proposal):
"""Enforce min <= value <= max"""
max = proposal["value"]
max = self._validate_tz(max)
if self.min and max < self.min:
raise TraitError("setting max < min")
if self.value and max < self.value:
self.value = max
return max


@register
class NaiveDatetimePicker(DatetimePicker):
"""
Display a widget for picking naive datetimes (i.e. timezone unaware).
Parameters
----------
value: datetime.datetime
The current value of the widget.
disabled: bool
Whether to disable user changes.
min: datetime.datetime
The lower allowed datetime bound
max: datetime.datetime
The upper allowed datetime bound
Examples
--------
>>> import datetime
>>> import ipydatetime
>>> datetime_pick = ipydatetime.NaiveDatetimePicker()
>>> datetime_pick.value = datetime.datetime(2018, 09, 5, 12, 34, 3)
"""

# Replace the serializers and model names:

_model_name = Unicode("NaiveDatetimeModel").tag(sync=True)

value = Datetime(None, allow_none=True).tag(sync=True, **naive_serialization)

min = Datetime(None, allow_none=True).tag(sync=True, **naive_serialization)
max = Datetime(None, allow_none=True).tag(sync=True, **naive_serialization)

def _validate_tz(self, value):
if value.tzinfo is not None:
raise TraitError('%s values needs to be timezone unaware' % (self.__class__.__name__,))
return value
91 changes: 91 additions & 0 deletions packages/controls/src/widget_datetime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,94 @@ namespace Private {
return value ? dt_as_dt_string(value).split('T', 2)[1] : '';
}
}

export interface ISerializedNaiveDatetime {
/**
* full year
*/
year: number;

/**
* zero-based month (0 means January, 11 means December)
*/
month: number;

/**
* day of month
*/
date: number;

/**
* hour (24H format)
*/
hours: number;

/**
* minutes
*/
minutes: number;

/**
* seconds
*/
seconds: number;

/**
* millisconds
*/
milliseconds: number;
}

export function serialize_naive(
value: Date | null
): ISerializedNaiveDatetime | null {
if (value === null) {
return null;
} else {
return {
year: value.getFullYear(),
month: value.getMonth(),
date: value.getDate(),
hours: value.getHours(),
minutes: value.getMinutes(),
seconds: value.getSeconds(),
milliseconds: value.getMilliseconds()
};
}
}

export function deserialize_naive(
value: ISerializedNaiveDatetime
): Date | null {
if (value === null) {
return null;
} else {
const date = new Date();
date.setFullYear(value.year, value.month, value.date);
date.setHours(
value.hours,
value.minutes,
value.seconds,
value.milliseconds
);
return date;
}
}

export const naive_serializers = {
serialize: serialize_naive,
deserialize: deserialize_naive
};

export class NaiveDatetimeModel extends DatetimeModel {
defaults(): Backbone.ObjectHash {
return { ...super.defaults(), _model_name: 'NaiveDatetimeModel' };
}

static serializers: ISerializers = {
...CoreDescriptionModel.serializers,
value: naive_serializers,
min: naive_serializers,
max: naive_serializers
};
}
87 changes: 86 additions & 1 deletion packages/controls/test/src/widget_datetime_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
createTestModelFromSerialized
} from './utils';

import { DatetimeModel, DatetimeView } from '../../lib';
import { DatetimeModel, DatetimeView, NaiveDatetimeModel } from '../../lib';

describe('Datetime', () => {
const date = new Date();
Expand Down Expand Up @@ -112,4 +112,89 @@ describe('Datetime', () => {
expect(view.model).to.equal(model);
});
});

describe('NaiveDatetimeModel', () => {
it('should be createable', () => {
const model = createTestModel(NaiveDatetimeModel);
expect(model).to.be.an.instanceof(NaiveDatetimeModel);
expect(model.get('value')).to.be.a('null');
});

it('should be createable with a value', () => {
const state = { value: date };
const model = createTestModel(NaiveDatetimeModel, state);
expect(model).to.be.an.instanceof(NaiveDatetimeModel);
expect(model.get('value')).to.eql(date);
});

it('should serialize as expected', async () => {
const state_in = {
value: {
year: 2002,
month: 2,
date: 20,
hours: 20,
minutes: 2,
seconds: 20,
milliseconds: 2
}
};

const model = await createTestModelFromSerialized(
NaiveDatetimeModel,
state_in
);
model.widget_manager.register_model(
model.model_id,
Promise.resolve(model)
);

const state_out = await (model.widget_manager as DummyManager).get_state();
const models = Object.keys(state_out.state).map(
k => state_out.state[k].state
);
expect(models.length).to.eql(1);
expect(models[0]._model_name).to.be('NaiveDatetimeModel');
expect(models[0].value).to.eql(state_in.value);
});

it('should deserialize to Date object', async () => {
const state_in = {
value: {
year: 2002,
month: 2,
date: 20,
hours: 20,
minutes: 2,
seconds: 20,
milliseconds: 2
}
};

const model = await createTestModelFromSerialized(
NaiveDatetimeModel,
state_in
);
expect(model.get('value')).to.eql(new Date(2002, 2, 20, 20, 2, 20, 2));
});

it('should deserialize null', async () => {
const state_in = { value: null };

const model = await createTestModelFromSerialized(
NaiveDatetimeModel,
state_in
);
expect(model.get('value')).to.be.a('null');
});

it('should deserialize undefined', async () => {
const state_in = {};
const model = await createTestModelFromSerialized(
NaiveDatetimeModel,
state_in
);
expect(model.get('value')).to.be.a('null');
});
});
});
Loading

0 comments on commit d45ac7d

Please sign in to comment.