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