Skip to content

Commit 685c76a

Browse files
committed
Merge pull request #1525 from learning-unlimited/flag-search-rewrite
Flag search rewrite
2 parents 56d44c3 + 4d0c734 commit 685c76a

File tree

20 files changed

+1538
-480
lines changed

20 files changed

+1538
-480
lines changed

esp/esp/program/modules/handlers/classflagmodule.py

+16-171
Original file line numberDiff line numberDiff line change
@@ -33,36 +33,32 @@
3333
"""
3434
from django.db.models.query import Q
3535
from django.http import HttpResponseBadRequest, HttpResponse
36+
from django.http import HttpResponseRedirect
3637

37-
import datetime
38-
import json
39-
import operator
38+
from esp.program.modules.base import ProgramModuleObj
39+
from esp.program.modules.base import main_call, aux_call, needs_admin
40+
from esp.web.util import render_to_response
4041

41-
from esp.program.modules.base import ProgramModuleObj, main_call, aux_call, needs_admin
42-
from esp.cache import cache_function
43-
from esp.web.util import render_to_response
44-
from esp.utils.query_utils import nest_Q
45-
46-
from esp.program.models import ClassSubject, ClassFlag, ClassFlagType, ClassCategories
47-
from esp.program.models.class_ import STATUS_CHOICES_DICT
42+
from esp.program.models import ClassFlag, ClassFlagType
4843
from esp.program.forms import ClassFlagForm
4944
from esp.users.models import ESPUser
5045

5146

5247
class ClassFlagModule(ProgramModuleObj):
53-
doc = """ Flag classes, such as for further review. Find all classes matching certain flags, and so on. """
48+
doc = """Flag classes, such as for further review."""
49+
5450
@classmethod
5551
def module_properties(cls):
5652
return {
57-
"admin_title": "Class Flags",
58-
"link_title": "Manage Class Flags",
59-
"module_type": "manage",
60-
"seq": 100,
61-
}
53+
"admin_title": "Class Flags",
54+
"link_title": "Manage Class Flags",
55+
"module_type": "manage",
56+
"seq": 100,
57+
}
6258

6359
class Meta:
6460
proxy = True
65-
61+
6662
def teachers(self, QObject = False):
6763
fts = ClassFlagType.get_flag_types(self.program)
6864
t = {}
@@ -74,170 +70,19 @@ def teachers(self, QObject = False):
7470
else:
7571
t['flag_%s' % flag_type.id] = ESPUser.objects.filter(q).distinct()
7672
return t
77-
73+
7874
def teacherDesc(self):
7975
fts = ClassFlagType.get_flag_types(self.program)
8076
descs = {}
8177
for flag_type in fts:
8278
descs['flag_%s' % flag_type.id] = """Teachers who have a class with the "%s" flag.""" % flag_type.name
8379
return descs
8480

85-
def jsonToQuerySet(self, j):
86-
'''Takes a dict from classflags and returns a QuerySet.
87-
88-
The dict is decoded from the json sent by the javascript in
89-
/manage///classflags/; the format is specified in the docstring of
90-
classflags() below.
91-
'''
92-
base = ClassSubject.objects.filter(parent_program=self.program)
93-
time_fmt = "%m/%d/%Y %H:%M"
94-
query_type = j['type']
95-
value = j.get('value')
96-
if 'flag' in query_type:
97-
lookups = {}
98-
if 'id' in value:
99-
lookups['flag_type'] = value['id']
100-
for time_type in ['created', 'modified']:
101-
when = value.get(time_type + '_when')
102-
lookup = time_type + '_time'
103-
if when == 'before':
104-
lookup += '__lt'
105-
elif when == 'after':
106-
lookup += '__gt'
107-
if when:
108-
lookups[lookup] = datetime.datetime.strptime(value[time_type+'_time'], time_fmt)
109-
if 'not' in query_type:
110-
# Due to https://code.djangoproject.com/ticket/14645, we have
111-
# to write this query a little weirdly.
112-
return base.exclude(id__in=ClassFlag.objects.filter(**lookups).values('subject'))
113-
else:
114-
return base.filter(nest_Q(Q(**lookups), 'flags'))
115-
elif query_type == 'category':
116-
return base.filter(category=value)
117-
elif query_type == 'not category':
118-
return base.exclude(category=value)
119-
elif query_type == 'status':
120-
return base.filter(status=value)
121-
elif query_type == 'not status':
122-
return base.exclude(status=value)
123-
elif 'scheduled' in query_type:
124-
lookup = 'sections__meeting_times__isnull'
125-
if 'some sections' in query_type:
126-
# Get classes with sections with meeting times.
127-
return base.filter(**{lookup: False})
128-
elif 'not all sections' in query_type:
129-
# Get classes with sections with meeting times.
130-
return base.filter(**{lookup: True})
131-
elif 'all sections' in query_type:
132-
# Exclude classes with sections with no meeting times.
133-
return base.exclude(**{lookup: True})
134-
elif 'no sections' in query_type:
135-
# Exclude classes with sections with meeting times.
136-
return base.exclude(**{lookup: False})
137-
else:
138-
# Here value is going to be a list of subqueries. First, evaluate them.
139-
subqueries = [self.jsonToQuerySet(query_json) for query_json in value]
140-
if query_type == 'all':
141-
return reduce(operator.and_, subqueries)
142-
elif query_type == 'any':
143-
return reduce(operator.or_, subqueries)
144-
elif query_type == 'none':
145-
return base.exclude(pk__in=reduce(operator.or_, subqueries))
146-
elif query_type == 'not all':
147-
return base.exclude(pk__in=reduce(operator.and_, subqueries))
148-
else:
149-
raise ESPError('Invalid json for flag query builder!')
150-
151-
def jsonToEnglish(self, j):
152-
'''Takes a dict from classflags and returns something human-readable.
153-
154-
The dict is decoded from the json sent by the javascript in
155-
/manage///classflags/; the format is specified in the docstring of
156-
classflags() below.
157-
'''
158-
query_type = j['type']
159-
value = j.get('value')
160-
if 'flag' in query_type:
161-
if 'id' in value:
162-
base = (query_type[:-4] + 'the flag "' +
163-
ClassFlagType.objects.get(id=value['id']).name + '"')
164-
elif 'not' in query_type:
165-
base = 'not flags'
166-
else:
167-
base = 'any flag'
168-
modifiers = []
169-
for time_type in ['created', 'modified']:
170-
if time_type+'_when' in value:
171-
modifiers.append(time_type + " " +
172-
value[time_type + '_when'] + " " +
173-
value[time_type + '_time'])
174-
base += ' '+' and '.join(modifiers)
175-
return base
176-
elif 'category' in query_type:
177-
return (query_type[:-8] + 'the category "' +
178-
str(ClassCategories.objects.get(id=value)) + '"')
179-
elif 'status' in query_type:
180-
statusname = STATUS_CHOICES_DICT[int(value)].capitalize()
181-
return query_type[:-6]+'the status "'+statusname+'"'
182-
elif 'scheduled' in query_type:
183-
return query_type
184-
else:
185-
subqueries = [self.jsonToEnglish(query) for query in value]
186-
return query_type+" of ("+', '.join(subqueries)+")"
187-
18881
@main_call
18982
@needs_admin
19083
def classflags(self, request, tl, one, two, module, extra, prog):
191-
'''An interface to query for some boolean expression of flags.
192-
193-
The front-end javascript will allow the user to build a query, then
194-
POST it in the form of a json. The response to said post should be the
195-
list of classes matching the flag query.
196-
197-
The json should be a single object, with keys 'type' and 'value'. The
198-
type of 'value' depends on the value of 'type':
199-
* If 'type' is 'flag' or 'not flag', 'value' should be an object,
200-
with some or all of the keys 'id', 'created_time', 'modified_time'
201-
(all should be strings).
202-
* If 'type' is 'category', 'not category', 'status', or
203-
'not status', 'value' should be a string.
204-
* If 'type' is 'some sections scheduled',
205-
'not all sections scheduled', 'all sections scheduled', or
206-
'no sections scheduled', 'value' should be omitted.
207-
* If 'type' is 'all', 'any', 'none', or 'not all', 'value' should
208-
be an array of objects of the same form.
209-
'''
210-
# Grab the data from either a GET or a POST.
211-
# We allow a GET request to make them linkable, and POST requests for
212-
# some kind of backwards-compatibility with the way the interface
213-
# previously worked.
214-
if request.method == 'GET':
215-
if 'query' in request.GET:
216-
data = request.GET['query']
217-
else:
218-
data = None
219-
else:
220-
data = request.POST['query']
221-
context = {
222-
'flag_types': ClassFlagType.get_flag_types(self.program),
223-
'prog': self.program,
224-
}
225-
if data is None:
226-
# We should display the query builder interface.
227-
fts = ClassFlagType.get_flag_types(self.program)
228-
context['categories'] = self.program.class_categories.all()
229-
return render_to_response(self.baseDir()+'flag_query_builder.html', request, context)
230-
else:
231-
# They've sent a query, let's process it.
232-
decoded = json.loads(data)
233-
# The prefetch lets us do basically all of the processing on the template level.
234-
queryset = self.jsonToQuerySet(decoded).distinct().order_by('id').prefetch_related(
235-
'flags', 'flags__flag_type', 'teachers', 'category', 'sections')
236-
english = self.jsonToEnglish(decoded)
237-
context['queryset']=queryset
238-
context['english']=english
239-
return render_to_response(self.baseDir()+'flag_results.html', request, context)
240-
84+
"""Deprecated, use the ClassSearchModule instead."""
85+
return HttpResponseRedirect('classsearch')
24186

24287
@aux_call
24388
@needs_admin
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import json
2+
3+
from django.db.models.query import Q
4+
5+
from esp.program.modules.base import ProgramModuleObj, main_call, needs_admin
6+
from esp.program.models.class_ import ClassSubject, STATUS_CHOICES
7+
from esp.program.models.flags import ClassFlagType
8+
from esp.utils.query_builder import QueryBuilder, SearchFilter
9+
from esp.utils.query_builder import SelectInput, ConstantInput, TextInput
10+
from esp.utils.query_builder import OptionalInput, DatetimeInput
11+
from esp.web.util import render_to_response
12+
13+
# TODO: this won't work right without class flags enabled
14+
15+
16+
class ClassSearchModule(ProgramModuleObj):
17+
"""Search for classes matching certain criteria."""
18+
@classmethod
19+
def module_properties(cls):
20+
return {
21+
"admin_title": "Class Search",
22+
"link_title": "Search for classes",
23+
"module_type": "manage",
24+
"seq": 10,
25+
}
26+
27+
class Meta:
28+
proxy = True
29+
30+
def query_builder(self):
31+
flag_types = ClassFlagType.get_flag_types(program=self.program)
32+
flag_datetime_inputs = [
33+
OptionalInput(name=t, inner=DatetimeInput(
34+
field_name='flags__%s_time' % t, english_name=''))
35+
for t in ['created', 'modified']]
36+
flag_select_input = SelectInput(
37+
field_name='flags__flag_type', english_name='type',
38+
options={str(ft.id): ft.name for ft in flag_types})
39+
flag_filter = SearchFilter(name='flag', title='the flag',
40+
inputs=[flag_select_input] +
41+
flag_datetime_inputs)
42+
any_flag_filter = SearchFilter(name='any_flag', title='any flag',
43+
inputs=flag_datetime_inputs)
44+
45+
categories = list(self.program.class_categories.all())
46+
if self.program.open_class_registration:
47+
categories.append(self.program.open_class_category)
48+
category_filter = SearchFilter(
49+
name='category', title='the category',
50+
inputs=[SelectInput(field_name='category', english_name='',
51+
options={str(cat.id): cat.category
52+
for cat in categories})])
53+
54+
status_filter = SearchFilter(
55+
name='status', title='the status',
56+
inputs=[SelectInput(field_name='status', english_name='', options={
57+
str(k): v for k, v in STATUS_CHOICES})])
58+
title_filter = SearchFilter(
59+
name='title', title='title containing',
60+
inputs=[TextInput(field_name='title__icontains', english_name='')])
61+
username_filter = SearchFilter(
62+
name='username', title='teacher username containing',
63+
inputs=[TextInput(field_name='teachers__username__contains',
64+
english_name='')])
65+
all_scheduled_filter = SearchFilter(
66+
name="all_scheduled", title="all sections scheduled",
67+
# Exclude classes with sections with null meeting times
68+
inverted=True,
69+
inputs=[ConstantInput(Q(sections__meeting_times__isnull=True))],
70+
)
71+
some_scheduled_filter = SearchFilter(
72+
name="some_scheduled", title="some sections scheduled",
73+
# Get classes with sections with non-null meeting times
74+
inputs=[ConstantInput(Q(sections__meeting_times__isnull=False))],
75+
)
76+
77+
return QueryBuilder(
78+
base=ClassSubject.objects.filter(parent_program=self.program),
79+
english_name="classes",
80+
filters=[
81+
status_filter,
82+
category_filter,
83+
title_filter,
84+
username_filter,
85+
flag_filter,
86+
any_flag_filter,
87+
all_scheduled_filter,
88+
some_scheduled_filter,
89+
])
90+
91+
@main_call
92+
@needs_admin
93+
def classsearch(self, request, tl, one, two, module, extra, prog):
94+
data = request.GET.get('query')
95+
query_builder = self.query_builder()
96+
if data is None:
97+
# Display a blank query builder
98+
context = {
99+
'query_builder': query_builder,
100+
'program': self.program,
101+
}
102+
return render_to_response(self.baseDir()+'query_builder.html',
103+
request, context)
104+
else:
105+
decoded = json.loads(data)
106+
queryset = query_builder.as_queryset(decoded)
107+
queryset = queryset.distinct().order_by('id').prefetch_related(
108+
'flags', 'flags__flag_type', 'teachers', 'category',
109+
'sections')
110+
english = query_builder.as_english(decoded)
111+
context = {
112+
'queryset': queryset,
113+
'english': english,
114+
'program': self.program,
115+
}
116+
return render_to_response(self.baseDir()+'search_results.html',
117+
request, context)

esp/esp/program/modules/tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@
4747
from esp.program.modules.tests.resourcemodule import ResourceModuleTest
4848
from esp.program.modules.tests.admincore import RegistrationTypeManagementTest
4949
from esp.program.modules.tests.adminclass import CancelClassTest
50+
from esp.program.modules.tests.classsearchmodule import ClassSearchModuleTest

0 commit comments

Comments
 (0)