Passed
Pull Request — 2.x (#1903)
by Ramon
04:34
created

DatetimeDataConverter.toWidgetValue()   A

Complexity

Conditions 3

Size

Total Lines 23
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 23
rs 10
c 0
b 0
f 0
cc 3
nop 2
1
# -*- coding: utf-8 -*-
2
3
from datetime import datetime
4
from datetime import timedelta
5
6
from bika.lims import api
7
from senaite.core.api import dtime
8
from senaite.core.interfaces import ISenaiteFormLayer
9
from senaite.core.schema.interfaces import IDatetimeField
10
from senaite.core.z3cform.interfaces import IDatetimeWidget
11
from z3c.form import interfaces
12
from z3c.form.browser import widget
13
from z3c.form.browser.widget import HTMLInputWidget
14
from z3c.form.converter import BaseDataConverter
15
from z3c.form.interfaces import IDataManager
16
from z3c.form.interfaces import IFieldWidget
17
from z3c.form.validator import SimpleFieldValidator
18
from z3c.form.widget import FieldWidget
19
from z3c.form.widget import Widget
20
from zope.component import adapter
21
from zope.component import queryMultiAdapter
22
from zope.interface import Interface
23
from zope.interface import implementer
24
25
HOUR_FORMAT = "%H:%M"
26
DATE_FORMAT = "%Y-%m-%d"
27
28
DATE_ONLY = ("{value.year:}-{value.month:02}-{value.day:02}")
29
30
DATE_AND_TIME = ("{value.year:}-{value.month:02}-{value.day:02} "
31
                 "{value.hour:02}:{value.minute:02}")
32
33
34
@adapter(
35
    Interface,           # context
36
    ISenaiteFormLayer,   # request
37
    interfaces.IForm,    # form
38
    IDatetimeField,      # field
39
    interfaces.IWidget,  # widget
40
)
41
class DatetimeDataValidator(SimpleFieldValidator):
42
    """Datetime validator
43
44
    Adapter looked up by `z3c.form.field.extract()` when storing a new value
45
    """
46
    def validate(self, value, force=True):
47
        # we always force to avoid comparison of datetime objects, which might
48
        # eventually raise this error `TypeError`:
49
        #
50
        # TypeError: can't compare offset-naive and offset-aware datetimes
51
        return super(DatetimeDataValidator, self).validate(value, force=True)
52
53
54
@adapter(IDatetimeField, interfaces.IWidget)
55
class DatetimeDataConverter(BaseDataConverter):
56
    """Converts the value between the field and the widget
57
    """
58
    def toWidgetValue(self, value):
59
        """Converts from field value to widget.
60
61
        This value will be set to the hidden field and does not reflect the
62
        date that is visible to the user!
63
64
        called by `z3c.form.widget.update`
65
66
        Bases:
67
            `plone.app.z3cform.converters.DatetimeWidgetConverter`
68
            `z3c.form.converter.DatetimeDataConverter`
69
70
        :param value: Field value.
71
        :type value: datetime
72
73
        :returns: Datetime in format `Y-m-d H:M`
74
        :rtype: string
75
        """
76
        if value is self.field.missing_value:
77
            return u""
78
        if getattr(self.widget, "show_time", None) is False:
79
            return DATE_ONLY.format(value=value)
80
        return DATE_AND_TIME.format(value=value)
81
82
    def toFieldValue(self, value):
83
        """Converts from widget (date string) value to field value (datetime)
84
85
        :param value: Date string inserted by datetime widget.
86
        :type value: string
87
88
        :returns: `datetime.datetime` object.
89
        :rtype: datetime
90
        """
91
        default = self.field.missing_value
92
        timezone = self.widget.default_timezone or dtime.get_os_timezone()
93
        return to_datetime(value, timezone=timezone, default=default)
94
95
96
def to_datetime(value, timezone=None, default=None):
97
    """Convert the value to a datetime object
98
99
    Code originally taken from here:
100
    `plone.app.z3cform.converters.DatetimeWidgetConverter.toFieldValue`
101
102
    :param value: Value inserted by datetime widget.
103
    :type value: string
104
    """
105
    if isinstance(value, datetime):
106
        return value
107
    if not value:
108
        return default
109
    tmp = value.split(" ")
110
    if not tmp[0]:
111
        return default
112
    value = tmp[0].split("-")
113
    if len(tmp) == 2 and ":" in tmp[1]:
114
        value += tmp[1].split(":")
115
    else:
116
        value += ["00", "00"]
117
118
    ret = datetime(*map(int, value))
119
    if timezone:
120
        if callable(timezone):
121
            timezone = timezone()
122
        ret = dtime.to_zone(ret, timezone)
123
    return ret
124
125
126
@implementer(IDatetimeWidget)
127
class DatetimeWidget(HTMLInputWidget, Widget):
128
    """Senaite date and time widget
129
    """
130
    klass = u"senaite-datetime-widget"
131
    # Olson DB/pytz timezone identifier or a callback
132
    # returning such an identifier.
133
    default_timezone = None
134
    # enable/disable time component
135
    show_time = True
136
    # disable past dates in the date picker
137
    datepicker_nopast = False
138
    # disable future dates in the date picker
139
    datepicker_nofuture = False
140
141
    def __init__(self, request, *args, **kw):
142
        super(DatetimeWidget, self).__init__(request)
143
        self.request = request
144
145
    def update(self):
146
        """Computes self.value for the widget templates
147
148
        see z3c.form.widget.Widget
149
        """
150
        super(DatetimeWidget, self).update()
151
        widget.addFieldClass(self)
152
153
    def to_localized_time(self, time, long_format=None, time_only=None):
154
        """Convert time to localized time
155
        """
156
        if dtime.is_dt(time):
157
            # NOTE: ts.ulocalized_time converts the value into a DateTime
158
            #       object, which always uses the current timezone without
159
            #       daylight savings, which might result in an offset.
160
            time = time.isoformat()
161
        ts = api.get_tool("translation_service")
162
        long_format = True if self.show_time else False
163
        return ts.ulocalized_time(
164
            time, long_format, time_only, self.context, domain="senaite.core")
165
166
    def get_display_value(self):
167
        """Returns the localized date value
168
        """
169
        value = self.value
170
        dm = queryMultiAdapter((self.context, self.field), IDataManager)
171
        if dm:
172
            # extract the object from the database
173
            value = dm.query()
174
        return self.to_localized_time(value)
175
176
    def to_datetime(self, value):
177
        """convert date string to datetime object with tz
178
179
        :param value: date or datetime
180
        :type value: string or datetime object
181
        :returns: datetime object
182
        """
183
        default = self.field.missing_value
184
        timezone = self.default_timezone
185
        return to_datetime(value, timezone=timezone, default=default)
186
187
    def get_date(self, value):
188
        """Return only the date part of the value
189
190
        :returns: date string
191
        """
192
        dt = self.to_datetime(value)
193
        if not dt:
194
            return u""
195
        return dt.strftime(DATE_FORMAT)
196
197
    def get_time(self, value):
198
        """Return only the time part of the value
199
200
        :returns: time string
201
        """
202
        dt = self.to_datetime(value)
203
        if not dt:
204
            return u""
205
        return dt.strftime(HOUR_FORMAT)
206
207
    def date_now(self, offset=0):
208
        """Get the current date without time component
209
        """
210
        ts = datetime.now().strftime(DATE_FORMAT)
211
        dt = datetime.strptime(ts, DATE_FORMAT)
212
        if offset:
213
            dt = dt + timedelta(offset)
214
        return dt
215
216
    def get_max(self):
217
        """Return the max allowed date in the future
218
219
        :returns: date string
220
        """
221
        now = self.date_now()
222
        return now.strftime(DATE_FORMAT)
223
224
    def get_min(self):
225
        """Return the min allowed date in the past
226
227
        :returns: date string
228
        """
229
        now = self.date_now()
230
        return now.strftime(DATE_FORMAT)
231
232
    def attrs(self):
233
        """Return the template attributes for the date field
234
235
        :returns: dictionary of HTML attributes
236
        """
237
        attrs = {}
238
        if self.datepicker_nofuture:
239
            attrs["max"] = self.get_max()
240
        if self.datepicker_nopast:
241
            attrs["min"] = self.get_min()
242
        return attrs
243
244
    @property
245
    def portal(self):
246
        """Return the portal object
247
        """
248
        return api.get_portal()
249
250
    @property
251
    def portal_url(self):
252
        """Return the portal object URL
253
        """
254
        return api.get_url(self.portal)
255
256
257
@adapter(IDatetimeField, ISenaiteFormLayer)
258
@implementer(IFieldWidget)
259
def DatetimeWidgetFactory(field, request):
260
    """Widget factory for datetime field
261
    """
262
    return FieldWidget(field, DatetimeWidget(request))
263