1#
2# BitBake Toaster Implementation
3#
4# Copyright (C) 2015        Intel Corporation
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9from django.db.models import Q, Max, Min
10from django.utils import dateparse, timezone
11from datetime import timedelta
12
13class TableFilter(object):
14    """
15    Stores a filter for a named field, and can retrieve the action
16    requested from the set of actions for that filter;
17    the order in which actions are added governs the order in which they
18    are returned in the JSON for the filter
19    """
20
21    def __init__(self, name, title):
22        self.name = name
23        self.title = title
24        self.__filter_action_map = {}
25
26        # retains the ordering of actions
27        self.__filter_action_keys = []
28
29    def add_action(self, action):
30        self.__filter_action_keys.append(action.name)
31        self.__filter_action_map[action.name] = action
32
33    def get_action(self, action_name):
34        return self.__filter_action_map[action_name]
35
36    def to_json(self, queryset):
37        """
38        Dump all filter actions as an object which can be JSON serialised;
39        this is used to generate the JSON for processing in
40        table.js / filterOpenClicked()
41        """
42        filter_actions = []
43
44        # add the "all" pseudo-filter action, which just selects the whole
45        # queryset
46        filter_actions.append({
47            'action_name' : 'all',
48            'title' : 'All',
49            'type': 'toggle',
50            'count' : queryset.count()
51        })
52
53        # add other filter actions
54        for action_name in self.__filter_action_keys:
55            filter_action = self.__filter_action_map[action_name]
56            obj = filter_action.to_json(queryset)
57            obj['action_name'] = action_name
58            filter_actions.append(obj)
59
60        return {
61            'name': self.name,
62            'title': self.title,
63            'filter_actions': filter_actions
64        }
65
66class TableFilterQueryHelper(object):
67    def dateStringsToQ(self, field_name, date_from_str, date_to_str):
68        """
69        Convert the date strings from_date_str and to_date_str into a
70        set of args in the form
71
72          {'<field_name>__gte': <date from>, '<field_name>__lte': <date to>}
73
74        where date_from and date_to are Django-timezone-aware dates; then
75        convert that into a Django Q object
76
77        Returns the Q object based on those criteria
78        """
79
80        # one of the values required for the filter is missing, so set
81        # it to the one which was supplied
82        if date_from_str == '':
83            date_from_str = date_to_str
84        elif date_to_str == '':
85            date_to_str = date_from_str
86
87        date_from_naive = dateparse.parse_datetime(date_from_str + ' 00:00:00')
88        date_to_naive = dateparse.parse_datetime(date_to_str + ' 23:59:59')
89
90        tz = timezone.get_default_timezone()
91        date_from = timezone.make_aware(date_from_naive, tz)
92        date_to = timezone.make_aware(date_to_naive, tz)
93
94        args = {}
95        args[field_name + '__gte'] = date_from
96        args[field_name + '__lte'] = date_to
97
98        return Q(**args)
99
100class TableFilterAction(object):
101    """
102    A filter action which displays in the filter popup for a ToasterTable
103    and uses an associated QuerysetFilter to filter the queryset for that
104    ToasterTable
105    """
106
107    def __init__(self, name, title, criteria):
108        self.name = name
109        self.title = title
110        self.criteria = criteria
111
112        # set in subclasses
113        self.type = None
114
115    def set_filter_params(self, params):
116        """
117        params: (str) a string of extra parameters for the action;
118        the structure of this string depends on the type of action;
119        it's ignored for a toggle filter action, which is just on or off
120        """
121        pass
122
123    def filter(self, queryset):
124        if self.criteria:
125            return queryset.filter(self.criteria)
126        else:
127            return queryset
128
129    def to_json(self, queryset):
130        """ Dump as a JSON object """
131        return {
132            'title': self.title,
133            'type': self.type,
134            'count': self.filter(queryset).count()
135        }
136
137class TableFilterActionToggle(TableFilterAction):
138    """
139    A single filter action which will populate one radio button of
140    a ToasterTable filter popup; this filter can either be on or off and
141    has no other parameters
142    """
143
144    def __init__(self, *args):
145        super(TableFilterActionToggle, self).__init__(*args)
146        self.type = 'toggle'
147
148class TableFilterActionDay(TableFilterAction):
149    """
150    A filter action which filters according to the named datetime field and a
151    string representing a day ("today" or "yesterday")
152    """
153
154    TODAY = 'today'
155    YESTERDAY = 'yesterday'
156
157    def __init__(self, name, title, field, day,
158    query_helper = TableFilterQueryHelper()):
159        """
160        field: (string) the datetime field to filter by
161        day: (string) "today" or "yesterday"
162        """
163        super(TableFilterActionDay, self).__init__(name, title, None)
164        self.type = 'day'
165        self.field = field
166        self.day = day
167        self.query_helper = query_helper
168
169    def filter(self, queryset):
170        """
171        Apply the day filtering before returning the queryset;
172        this is done here as the value of the filter criteria changes
173        depending on when the filtering is applied
174        """
175
176        now = timezone.now()
177
178        if self.day == self.YESTERDAY:
179            increment = timedelta(days=1)
180            wanted_date = now - increment
181        else:
182            wanted_date = now
183
184        wanted_date_str = wanted_date.strftime('%Y-%m-%d')
185
186        self.criteria = self.query_helper.dateStringsToQ(
187            self.field,
188            wanted_date_str,
189            wanted_date_str
190        )
191
192        return queryset.filter(self.criteria)
193
194class TableFilterActionDateRange(TableFilterAction):
195    """
196    A filter action which will filter the queryset by a date range.
197    The date range can be set via set_params()
198    """
199
200    def __init__(self, name, title, field,
201    query_helper = TableFilterQueryHelper()):
202        """
203        field: (string) the field to find the max/min range from in the queryset
204        """
205        super(TableFilterActionDateRange, self).__init__(
206            name,
207            title,
208            None
209        )
210
211        self.type = 'daterange'
212        self.field = field
213        self.query_helper = query_helper
214
215    def set_filter_params(self, params):
216        """
217        This filter depends on the user selecting some input, so it needs
218        to have its parameters set before its queryset is filtered
219
220        params: (str) a string of extra parameters for the filtering
221        in the format "2015-12-09,2015-12-11" (from,to); this is passed in the
222        querystring and used to set the criteria on the QuerysetFilter
223        associated with this action
224        """
225
226        # if params are invalid, return immediately, resetting criteria
227        # on the QuerysetFilter
228        try:
229            date_from_str, date_to_str = params.split(',')
230        except ValueError:
231            self.criteria = None
232            return
233
234        # one of the values required for the filter is missing, so set
235        # it to the one which was supplied
236        self.criteria = self.query_helper.dateStringsToQ(
237            self.field,
238            date_from_str,
239            date_to_str
240        )
241
242    def to_json(self, queryset):
243        """ Dump as a JSON object """
244        data = super(TableFilterActionDateRange, self).to_json(queryset)
245
246        # additional data about the date range covered by the queryset's
247        # records, retrieved from its <field> column
248        data['min'] = queryset.aggregate(Min(self.field))[self.field + '__min']
249        data['max'] = queryset.aggregate(Max(self.field))[self.field + '__max']
250
251        # a range filter has a count of None, as the number of records it
252        # will select depends on the date range entered and we don't know
253        # that ahead of time
254        data['count'] = None
255
256        return data
257
258class TableFilterMap(object):
259    """
260    Map from field names to TableFilter objects for those fields
261    """
262
263    def __init__(self):
264        self.__filters = {}
265
266    def add_filter(self, filter_name, table_filter):
267        """ table_filter is an instance of Filter """
268        self.__filters[filter_name] = table_filter
269
270    def get_filter(self, filter_name):
271        return self.__filters[filter_name]
272
273    def to_json(self, queryset):
274        data = {}
275
276        for filter_name, table_filter in self.__filters.items():
277            data[filter_name] = table_filter.to_json()
278
279        return data
280