~dylanmccall/harvest/gsoc-client-stuff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
#FIXME: The order that items are rendered in ChoiceFilter and FilterGroup needs to be as specified in the constructor. Currently, we are not in control of the order.
#TODO: adjust Filter.render() methods for custom template tags and use django.template.Template

from containers import FilterContainer, FilterSystem
from django.utils.safestring import mark_safe
from copy import copy

class Filter(object): #abstract, extend in application
    """
    The abstract base class for all other filters.
    
    Every Filter's main objective is to process QuerySets with the method
    Filter.process_queryset. Filter, or one of its subclasses, should be
    extended with a new version of process_queryset that does something useful
    for a specific application. For example, process_queryset could return
    queryset.filter(foo="bar") so using the filter will limit the queryset
    to objects where foo=bar.
    
    Every Filter has a unique id and can belong to a single container.
    
    A Filter can be assigned a new value from a string and its internal value
    (which could be of any type) can be serialized back to a string.
    The object itself can be serialized as a key / value pair, where the key
    is completely unique.
    
    Every Filter can be rendered, which outputs markup (currently html)
    describing it for a user interface.
    """
    
    #TODO: generate this automatically
    html_class = 'filter'
    """
    The class that this object should have in its html representation.
    A subclass should override this property to list all Filter classes
    it inherits from, separated by spaces and in lower case.
    """
    
    def __init__(self, id_str, **kwargs): #final
        self.id_str = id_str #local name
        self.container = None #immediate container (FilterContainer)
        
        self.system = None #cache for self.get_system
        self.full_name = None #cache for self.get_full_name
        
        self.name = self.id_str
        if 'name' in kwargs:
            self.name = kwargs['name']
    
    def get_id(self): #final
        """
        @return: the local id of this filter, unique to its container.
        """
        return self.id_str
    
    def set_container(self, container): #final
        """
        Specify that this filter belongs to a specific container. This will
        replace any container it currently believes it belongs to.
        @param container: a FilterContainer object that holds this Filter
        """
        #it would make sense to raise an exception if self.container != None
        self.container = container
    
    def get_container(self): #final
        """
        @return: the container that this filter belongs to
        """
        return self.container
    
    def get_system(self): #final
        """
        Returns the FilterSystem that this filter ultimately belongs to.
        All objects in the system need to be created by the time this
        method is called.
        @return: the root FilterSystem
        """
        if self.system == None:
            container = self.get_container()
            system = None
            if isinstance(container, Filter): 
                system = container.get_system()
            elif isinstance(container, FilterSystem):
                system = container
            self.system = system
        return self.system
    
    def get_full_name(self): #final
        """
        Returns the filter's full name, which should make sense from anywhere
        in the application. This name is in the format parent:child:child.
        All objects in the system need to be created by the time this
        method is called.
        @return: the filter's full name, which is a string
        """
        if self.full_name == None:
            full_name = self.get_id()
            container = self.get_container()
            if isinstance(container, Filter):
                full_name = "%s:%s" % (container.get_full_name(), full_name)
            self.full_name = full_name
        return self.full_name
    
    def set_value(self, value): #abstract
        """
        Extend this to take a value passed down from the top, probably
        from FilterSystem.update_from_http, and do something with it.
        @param value: a new value, as a string
        """
        raise NotImplementedError(self.set_value)
    
    def get_value(self): #abstract
        """
        @return: a copy of this filter's value in its native format
        """
        raise NotImplementedError(self.get_value)
    
    def serialize(self, value = None): #final
        """
        Creates a dictionary of parameters to describe this object, either
        as-is or with a new value.
        The result can be sent to FilterSystem.get_url_with_parameters.
        @param value: a different value to use, in a format native to this Filter (see get_value)
        @return: a dictionary of key:value pairs referring to the object and its value.
        """
        if value == None: value = self.get_value()
        key = self.get_full_name()
        value_str = self.serialize_value(value)
        return {key : value_str}
    
    def serialize_value(self, value): #abstract
        """
        The inverse of set_value. Returns the given value as a string that could
        be added to an HTTP query string.
        @param value: the value to serialize, in a format native to this Filter (see get_value)
        @return: a unicode string formatted for set_value
        """
        raise NotImplementedError(self.serialize_value)
    
    def process_queryset(self, queryset): #abstract
        """
        Extend this to manipulate a given queryset and then return it.
        For example, queryset.filter(name__startswith = self.value)
        @param queryset: a queryset to operate on
        @return: a queryset based on the given one
        """
        raise NotImplementedError(self.process_queryset)
    
    def render(self, **kwargs): #final
        """
        @return: the default rendering of the filter itself in given context
        """
        return self.render_html()
    
    def render_html(self, **kwargs): #final
        """
        @return: the entire filter, rendered as html
        """
        inner_html = self.render_html_inner(**kwargs)
        
        #safe because we are in full control of this content
        return mark_safe(
            u'<span class="%s" data-filter-fullname="%s">\n%s\n</span>' % \
            (self.html_class,
             self.get_full_name(),
             inner_html ))
    
    def render_html_inner(self, **kwargs):
        """
        Extend this to return the html output for the filter itself.
        (The inside part, without the <span class=filter> boilerplate).
        The output should be simple and semantically meaningful, with no
        specific concern about formatting. It will be placed within other
        tags that describe its context.
        @return: the filter's contents, rendered as html
        """
        label = self.render_html_label(**kwargs)
        if label: label = '<span class="filter-label">%s</span>' % label
        else: label = ''
        
        value = self.render_html_value(**kwargs)
        if value: value = '<span class="filter-value">%s</span>' % value
        else: value = ''
        
        return '%s\n%s' % (label, value)
    
    def render_html_label(self, **kwargs):
        """
        @return: the filter's label, rendered as html
        """
        link_url = None
        if 'toggle_href' in kwargs: link_url = kwargs['toggle_href']
        
        label = self.name
        if link_url:
            label = '<a class="item-toggle" href="%s">%s</a>' % (link_url, label)
        
        return label
    
    def render_html_value(self, **kwargs):
        """
        @return: the filter's value, rendered as html
        """
        return None


class EditFilter(Filter): #abstract, extend in application
    """
    This Filter has a simple string value which can be edited by the user.
    
    Serialized as stored ("value")
    """
    
    html_class = 'filter editfilter'
    
    def __init__(self, id_str, **kwargs):
        Filter.__init__(self, id_str, **kwargs)
        self.input_str = ""
    
    def set_value(self, value): #overrides Filter
        self.input_str = value
    
    def get_value(self): #overrides Filter
        return self.input_str
    
    def serialize_value(self, value): #overrides Filter
        return value
    
    def render_html_value(self, **kwargs):
        field_value = self.get_value()
        return '<input type="text" placeholder="(type here)" spellcheck="false" value="%s" />' % field_value


class SetFilter(Filter): #abstract, extend in application
    """
    Holds a set of strings, with no repetition.
    
    Serialized as a comma-separated list ("dog,cat,horse,mouse")
    """
    
    html_class = 'filter setfilter'
    
    def __init__(self, id_str, **kwargs):
        Filter.__init__(self, id_str, **kwargs)
        self.selected_set = set()
    
    def set_value(self, value): #overrides Filter
        self.selected_set = set([s for s in value.split(",") if self.id_allowed(s)])
    
    def get_value(self): #overrides Filter
        return self.selected_set.copy()
    
    def serialize_value(self, value): #overrides Filter
        return ",".join(value)
    
    def get_value_with_selection(self, item_id): #final
        """
        Returns the current value of this object with the selection referred to
        by item_id toggled on or off, depending on its current state.
        @param item_id: id for the item to toggle
        @return: the value of this SetFilter with the given item toggled on or off
        """
        select = self.get_value()
        if item_id in select:
            select.remove(item_id)
        else:
            select.add(item_id)
        return select
    
    def id_selected(self, item_id):
        return item_id in self.selected_set
    
    def id_allowed(self, item_id):
        return True


class ChoiceFilter(SetFilter): #abstract, extend in application
    """
    Has a dictionary of items, with names and values of any type. These can be
    selected or deselected by the input, which is a set of strings, as in
    SetFilter. In that set, any items which refer to choices that do not exist
    are ignored.
    
    Serialized as a comma-separated list, like SetFilter.
    """
    
    html_class = 'filter setfilter choicefilter'
    
    def __init__(self, id_str, choices_dict = None, **kwargs):
        SetFilter.__init__(self, id_str, **kwargs)
        if choices_dict:
            self.choices_dict = choices_dict
        else:
            self.choices_dict = self.default_choices_dict()
    
    def default_choices_dict(self):
        return dict()
    
    def id_allowed(self, item_id): #overrides SetFilter
        return item_id in self.choices_dict
    
    def get_selected_choices(self):
        return [self.choices_dict[s] for s in self.selected_set]
    
    def render_html_value(self, **kwargs):
        choices = ''
        
        for c_id in self.choices_dict:
            c_render = self.render_html_value_choice(c_id)
            li_params = 'data-item-id="%s"' % c_id
            checkbox_inner = ''
            if self.id_selected(c_id):
                li_params = '%s data-selected' % li_params
                checkbox_inner = '&#10003;' #a check mark
            checkbox = '<span class="checkbox">%s</span>' % checkbox_inner
            choices += '\n<li %s>\n%s\n%s\n</li>\n' % (li_params, checkbox, c_render)
        
        return '<ul>\n%s\n</ul>' % choices
    
    def render_html_value_choice(self, item_id):
        toggle_params = self.serialize(self.get_value_with_selection(item_id))
        item_href = self.get_system().get_url_with_parameters(toggle_params)
        
        return mark_safe(u'<a class="item-toggle" href="%s">%s</a>' % (item_href, item_id))


class FilterGroup(FilterContainer, ChoiceFilter): #final
    """
    A collection of other Filters, which are selected (enabled) according to the
    rules of ChoiceFilter.
    
    The do_queryset method combines the output from all selected Filters.
    
    Serialized as a comma-separated list, like ChoiceFilter.
    """
    
    html_class = 'filter setfilter choicefilter filtergroup'
    
    def __init__(self, id_str, filters_set, **kwargs):
        FilterContainer.__init__(self, filters_set)
        #self.filters_dict comes from FilterContainer's initializer
        ChoiceFilter.__init__(self, id_str, self.filters_dict, **kwargs)
    
    def process_queryset(self, queryset): #overrides Filter
        for f in self.get_selected_choices():
            queryset = f.process_queryset(queryset) #returns something like QuerySet.filter(blah)
        return queryset
    
    def render_html_value_choice(self, filter_id):
        toggle_params = self.serialize(self.get_value_with_selection(filter_id))
        toggle_href = self.get_system().get_url_with_parameters(toggle_params)
        
        f = self.filters_dict[filter_id]
        return f.render_html(toggle_href = toggle_href)