1import time
2import datetime
3import uuid
4
5from flask_admin.babel import lazy_gettext
6
7
8class BaseFilter(object):
9    """
10        Base filter class.
11    """
12    def __init__(self, name, options=None, data_type=None, key_name=None):
13        """
14            Constructor.
15
16            :param name:
17                Displayed name
18            :param options:
19                List of fixed options. If provided, will use drop down instead of textbox.
20            :param data_type:
21                Client-side widget type to use.
22            :param key_name:
23                Optional name who represent this filter.
24        """
25        self.name = name
26        self.options = options
27        self.data_type = data_type
28        self.key_name = key_name
29
30    def get_options(self, view):
31        """
32            Return list of predefined options.
33
34            Override to customize behavior.
35
36            :param view:
37                Associated administrative view class.
38        """
39        options = self.options
40
41        if options:
42            if callable(options):
43                options = options()
44
45            return options
46
47        return None
48
49    def validate(self, value):
50        """
51            Validate value.
52
53            If value is valid, returns `True` and `False` otherwise.
54
55            :param value:
56                Value to validate
57        """
58        # useful for filters with date conversions, see if conversion in clean() raises ValueError
59        try:
60            self.clean(value)
61            return True
62        except ValueError:
63            return False
64
65    def clean(self, value):
66        """
67            Parse value into python format. Occurs before .apply()
68
69            :param value:
70                Value to parse
71        """
72        return value
73
74    def apply(self, query, value):
75        """
76            Apply search criteria to the query and return new query.
77
78            :param query:
79                Query
80            :param value:
81                Search criteria
82        """
83        raise NotImplementedError()
84
85    def operation(self):
86        """
87            Return readable operation name.
88
89            For example: u'equals'
90        """
91        raise NotImplementedError()
92
93    def __unicode__(self):
94        return self.name
95
96
97# Customized filters
98class BaseBooleanFilter(BaseFilter):
99    """
100        Base boolean filter, uses fixed list of options.
101    """
102    def __init__(self, name, options=None, data_type=None):
103        super(BaseBooleanFilter, self).__init__(name,
104                                                (('1', lazy_gettext(u'Yes')),
105                                                 ('0', lazy_gettext(u'No'))),
106                                                data_type)
107
108    def validate(self, value):
109        return value in ('0', '1')
110
111
112class BaseIntFilter(BaseFilter):
113    """
114        Base Int filter. Adds validation and changes value to python int.
115
116        Avoid using int(float(value)) to also allow using decimals, because it
117        causes precision issues with large numbers.
118    """
119    def clean(self, value):
120        return int(value)
121
122
123class BaseFloatFilter(BaseFilter):
124    """
125        Base Float filter. Adds validation and changes value to python float.
126    """
127    def clean(self, value):
128        return float(value)
129
130
131class BaseIntListFilter(BaseFilter):
132    """
133        Base Integer list filter. Adds validation for int "In List" filter.
134
135        Avoid using int(float(value)) to also allow using decimals, because it
136        causes precision issues with large numbers.
137    """
138    def clean(self, value):
139        return [int(v.strip()) for v in value.split(',') if v.strip()]
140
141
142class BaseFloatListFilter(BaseFilter):
143    """
144        Base Float list filter. Adds validation for float "In List" filter.
145    """
146    def clean(self, value):
147        return [float(v.strip()) for v in value.split(',') if v.strip()]
148
149
150class BaseDateFilter(BaseFilter):
151    """
152        Base Date filter. Uses client-side date picker control.
153    """
154    def __init__(self, name, options=None, data_type=None):
155        super(BaseDateFilter, self).__init__(name,
156                                             options,
157                                             data_type='datepicker')
158
159    def clean(self, value):
160        return datetime.datetime.strptime(value, '%Y-%m-%d').date()
161
162
163class BaseDateBetweenFilter(BaseFilter):
164    """
165        Base Date Between filter. Consolidates logic for validation and clean.
166        Apply method is different for each back-end.
167    """
168    def clean(self, value):
169        return [datetime.datetime.strptime(range, '%Y-%m-%d').date()
170                for range in value.split(' to ')]
171
172    def operation(self):
173        return lazy_gettext('between')
174
175    def validate(self, value):
176        try:
177            value = [datetime.datetime.strptime(range, '%Y-%m-%d').date()
178                     for range in value.split(' to ')]
179            # if " to " is missing, fail validation
180            # sqlalchemy's .between() will not work if end date is before start date
181            if (len(value) == 2) and (value[0] <= value[1]):
182                return True
183            else:
184                return False
185        except ValueError:
186            return False
187
188
189class BaseDateTimeFilter(BaseFilter):
190    """
191        Base DateTime filter. Uses client-side date time picker control.
192    """
193    def __init__(self, name, options=None, data_type=None):
194        super(BaseDateTimeFilter, self).__init__(name,
195                                                 options,
196                                                 data_type='datetimepicker')
197
198    def clean(self, value):
199        # datetime filters will not work in SQLite + SQLAlchemy if value not converted to datetime
200        return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
201
202
203class BaseDateTimeBetweenFilter(BaseFilter):
204    """
205        Base DateTime Between filter. Consolidates logic for validation and clean.
206        Apply method is different for each back-end.
207    """
208    def clean(self, value):
209        return [datetime.datetime.strptime(range, '%Y-%m-%d %H:%M:%S')
210                for range in value.split(' to ')]
211
212    def operation(self):
213        return lazy_gettext('between')
214
215    def validate(self, value):
216        try:
217            value = [datetime.datetime.strptime(range, '%Y-%m-%d %H:%M:%S')
218                     for range in value.split(' to ')]
219            if (len(value) == 2) and (value[0] <= value[1]):
220                return True
221            else:
222                return False
223        except ValueError:
224            return False
225
226
227class BaseTimeFilter(BaseFilter):
228    """
229        Base Time filter. Uses client-side time picker control.
230    """
231    def __init__(self, name, options=None, data_type=None):
232        super(BaseTimeFilter, self).__init__(name,
233                                             options,
234                                             data_type='timepicker')
235
236    def clean(self, value):
237        # time filters will not work in SQLite + SQLAlchemy if value not converted to time
238        timetuple = time.strptime(value, '%H:%M:%S')
239        return datetime.time(timetuple.tm_hour,
240                             timetuple.tm_min,
241                             timetuple.tm_sec)
242
243
244class BaseTimeBetweenFilter(BaseFilter):
245    """
246        Base Time Between filter. Consolidates logic for validation and clean.
247        Apply method is different for each back-end.
248    """
249    def clean(self, value):
250        timetuples = [time.strptime(range, '%H:%M:%S')
251                      for range in value.split(' to ')]
252        return [
253            datetime.time(timetuple.tm_hour, timetuple.tm_min, timetuple.tm_sec)
254            for timetuple in timetuples
255        ]
256
257    def operation(self):
258        return lazy_gettext('between')
259
260    def validate(self, value):
261        try:
262            timetuples = [time.strptime(range, '%H:%M:%S')
263                          for range in value.split(' to ')]
264            if (len(timetuples) == 2) and (timetuples[0] <= timetuples[1]):
265                return True
266            else:
267                return False
268        except ValueError:
269            raise
270            return False
271
272
273class BaseUuidFilter(BaseFilter):
274    """
275        Base uuid filter
276    """
277    def __init__(self, name, options=None, data_type=None):
278        super(BaseUuidFilter, self).__init__(name,
279                                             options,
280                                             data_type='uuid')
281
282    def clean(self, value):
283        value = uuid.UUID(value)
284        return str(value)
285
286
287class BaseUuidListFilter(BaseFilter):
288    """
289        Base uuid list filter
290    """
291
292    def clean(self, value):
293        return [str(uuid.UUID(v.strip())) for v in value.split(',') if v.strip()]
294
295
296def convert(*args):
297    """
298        Decorator for field to filter conversion routine.
299
300        See :mod:`flask_admin.contrib.sqla.filters` for usage example.
301    """
302    def _inner(func):
303        func._converter_for = list(map(lambda x: x.lower(), args))
304        return func
305    return _inner
306
307
308class BaseFilterConverter(object):
309    """
310        Base filter converter.
311
312        Derive from this class to implement custom field to filter conversion
313        logic.
314    """
315    def __init__(self):
316        self.converters = dict()
317
318        for p in dir(self):
319            attr = getattr(self, p)
320
321            if hasattr(attr, '_converter_for'):
322                for p in attr._converter_for:
323                    self.converters[p] = attr
324