Skip to content

Commit

Permalink
Merge pull request #1525 from learning-unlimited/flag-search-rewrite
Browse files Browse the repository at this point in the history
Flag search rewrite
  • Loading branch information
LuaC committed Mar 29, 2015
2 parents 56d44c3 + 4d0c734 commit 685c76a
Show file tree
Hide file tree
Showing 20 changed files with 1,538 additions and 480 deletions.
187 changes: 16 additions & 171 deletions esp/esp/program/modules/handlers/classflagmodule.py
Expand Up @@ -33,36 +33,32 @@
"""
from django.db.models.query import Q
from django.http import HttpResponseBadRequest, HttpResponse
from django.http import HttpResponseRedirect

import datetime
import json
import operator
from esp.program.modules.base import ProgramModuleObj
from esp.program.modules.base import main_call, aux_call, needs_admin
from esp.web.util import render_to_response

from esp.program.modules.base import ProgramModuleObj, main_call, aux_call, needs_admin
from esp.cache import cache_function
from esp.web.util import render_to_response
from esp.utils.query_utils import nest_Q

from esp.program.models import ClassSubject, ClassFlag, ClassFlagType, ClassCategories
from esp.program.models.class_ import STATUS_CHOICES_DICT
from esp.program.models import ClassFlag, ClassFlagType
from esp.program.forms import ClassFlagForm
from esp.users.models import ESPUser


class ClassFlagModule(ProgramModuleObj):
doc = """ Flag classes, such as for further review. Find all classes matching certain flags, and so on. """
doc = """Flag classes, such as for further review."""

@classmethod
def module_properties(cls):
return {
"admin_title": "Class Flags",
"link_title": "Manage Class Flags",
"module_type": "manage",
"seq": 100,
}
"admin_title": "Class Flags",
"link_title": "Manage Class Flags",
"module_type": "manage",
"seq": 100,
}

class Meta:
proxy = True

def teachers(self, QObject = False):
fts = ClassFlagType.get_flag_types(self.program)
t = {}
Expand All @@ -74,170 +70,19 @@ def teachers(self, QObject = False):
else:
t['flag_%s' % flag_type.id] = ESPUser.objects.filter(q).distinct()
return t

def teacherDesc(self):
fts = ClassFlagType.get_flag_types(self.program)
descs = {}
for flag_type in fts:
descs['flag_%s' % flag_type.id] = """Teachers who have a class with the "%s" flag.""" % flag_type.name
return descs

def jsonToQuerySet(self, j):
'''Takes a dict from classflags and returns a QuerySet.
The dict is decoded from the json sent by the javascript in
/manage///classflags/; the format is specified in the docstring of
classflags() below.
'''
base = ClassSubject.objects.filter(parent_program=self.program)
time_fmt = "%m/%d/%Y %H:%M"
query_type = j['type']
value = j.get('value')
if 'flag' in query_type:
lookups = {}
if 'id' in value:
lookups['flag_type'] = value['id']
for time_type in ['created', 'modified']:
when = value.get(time_type + '_when')
lookup = time_type + '_time'
if when == 'before':
lookup += '__lt'
elif when == 'after':
lookup += '__gt'
if when:
lookups[lookup] = datetime.datetime.strptime(value[time_type+'_time'], time_fmt)
if 'not' in query_type:
# Due to https://code.djangoproject.com/ticket/14645, we have
# to write this query a little weirdly.
return base.exclude(id__in=ClassFlag.objects.filter(**lookups).values('subject'))
else:
return base.filter(nest_Q(Q(**lookups), 'flags'))
elif query_type == 'category':
return base.filter(category=value)
elif query_type == 'not category':
return base.exclude(category=value)
elif query_type == 'status':
return base.filter(status=value)
elif query_type == 'not status':
return base.exclude(status=value)
elif 'scheduled' in query_type:
lookup = 'sections__meeting_times__isnull'
if 'some sections' in query_type:
# Get classes with sections with meeting times.
return base.filter(**{lookup: False})
elif 'not all sections' in query_type:
# Get classes with sections with meeting times.
return base.filter(**{lookup: True})
elif 'all sections' in query_type:
# Exclude classes with sections with no meeting times.
return base.exclude(**{lookup: True})
elif 'no sections' in query_type:
# Exclude classes with sections with meeting times.
return base.exclude(**{lookup: False})
else:
# Here value is going to be a list of subqueries. First, evaluate them.
subqueries = [self.jsonToQuerySet(query_json) for query_json in value]
if query_type == 'all':
return reduce(operator.and_, subqueries)
elif query_type == 'any':
return reduce(operator.or_, subqueries)
elif query_type == 'none':
return base.exclude(pk__in=reduce(operator.or_, subqueries))
elif query_type == 'not all':
return base.exclude(pk__in=reduce(operator.and_, subqueries))
else:
raise ESPError('Invalid json for flag query builder!')

def jsonToEnglish(self, j):
'''Takes a dict from classflags and returns something human-readable.
The dict is decoded from the json sent by the javascript in
/manage///classflags/; the format is specified in the docstring of
classflags() below.
'''
query_type = j['type']
value = j.get('value')
if 'flag' in query_type:
if 'id' in value:
base = (query_type[:-4] + 'the flag "' +
ClassFlagType.objects.get(id=value['id']).name + '"')
elif 'not' in query_type:
base = 'not flags'
else:
base = 'any flag'
modifiers = []
for time_type in ['created', 'modified']:
if time_type+'_when' in value:
modifiers.append(time_type + " " +
value[time_type + '_when'] + " " +
value[time_type + '_time'])
base += ' '+' and '.join(modifiers)
return base
elif 'category' in query_type:
return (query_type[:-8] + 'the category "' +
str(ClassCategories.objects.get(id=value)) + '"')
elif 'status' in query_type:
statusname = STATUS_CHOICES_DICT[int(value)].capitalize()
return query_type[:-6]+'the status "'+statusname+'"'
elif 'scheduled' in query_type:
return query_type
else:
subqueries = [self.jsonToEnglish(query) for query in value]
return query_type+" of ("+', '.join(subqueries)+")"

@main_call
@needs_admin
def classflags(self, request, tl, one, two, module, extra, prog):
'''An interface to query for some boolean expression of flags.
The front-end javascript will allow the user to build a query, then
POST it in the form of a json. The response to said post should be the
list of classes matching the flag query.
The json should be a single object, with keys 'type' and 'value'. The
type of 'value' depends on the value of 'type':
* If 'type' is 'flag' or 'not flag', 'value' should be an object,
with some or all of the keys 'id', 'created_time', 'modified_time'
(all should be strings).
* If 'type' is 'category', 'not category', 'status', or
'not status', 'value' should be a string.
* If 'type' is 'some sections scheduled',
'not all sections scheduled', 'all sections scheduled', or
'no sections scheduled', 'value' should be omitted.
* If 'type' is 'all', 'any', 'none', or 'not all', 'value' should
be an array of objects of the same form.
'''
# Grab the data from either a GET or a POST.
# We allow a GET request to make them linkable, and POST requests for
# some kind of backwards-compatibility with the way the interface
# previously worked.
if request.method == 'GET':
if 'query' in request.GET:
data = request.GET['query']
else:
data = None
else:
data = request.POST['query']
context = {
'flag_types': ClassFlagType.get_flag_types(self.program),
'prog': self.program,
}
if data is None:
# We should display the query builder interface.
fts = ClassFlagType.get_flag_types(self.program)
context['categories'] = self.program.class_categories.all()
return render_to_response(self.baseDir()+'flag_query_builder.html', request, context)
else:
# They've sent a query, let's process it.
decoded = json.loads(data)
# The prefetch lets us do basically all of the processing on the template level.
queryset = self.jsonToQuerySet(decoded).distinct().order_by('id').prefetch_related(
'flags', 'flags__flag_type', 'teachers', 'category', 'sections')
english = self.jsonToEnglish(decoded)
context['queryset']=queryset
context['english']=english
return render_to_response(self.baseDir()+'flag_results.html', request, context)

"""Deprecated, use the ClassSearchModule instead."""
return HttpResponseRedirect('classsearch')

@aux_call
@needs_admin
Expand Down
117 changes: 117 additions & 0 deletions esp/esp/program/modules/handlers/classsearchmodule.py
@@ -0,0 +1,117 @@
import json

from django.db.models.query import Q

from esp.program.modules.base import ProgramModuleObj, main_call, needs_admin
from esp.program.models.class_ import ClassSubject, STATUS_CHOICES
from esp.program.models.flags import ClassFlagType
from esp.utils.query_builder import QueryBuilder, SearchFilter
from esp.utils.query_builder import SelectInput, ConstantInput, TextInput
from esp.utils.query_builder import OptionalInput, DatetimeInput
from esp.web.util import render_to_response

# TODO: this won't work right without class flags enabled


class ClassSearchModule(ProgramModuleObj):
"""Search for classes matching certain criteria."""
@classmethod
def module_properties(cls):
return {
"admin_title": "Class Search",
"link_title": "Search for classes",
"module_type": "manage",
"seq": 10,
}

class Meta:
proxy = True

def query_builder(self):
flag_types = ClassFlagType.get_flag_types(program=self.program)
flag_datetime_inputs = [
OptionalInput(name=t, inner=DatetimeInput(
field_name='flags__%s_time' % t, english_name=''))
for t in ['created', 'modified']]
flag_select_input = SelectInput(
field_name='flags__flag_type', english_name='type',
options={str(ft.id): ft.name for ft in flag_types})
flag_filter = SearchFilter(name='flag', title='the flag',
inputs=[flag_select_input] +
flag_datetime_inputs)
any_flag_filter = SearchFilter(name='any_flag', title='any flag',
inputs=flag_datetime_inputs)

categories = list(self.program.class_categories.all())
if self.program.open_class_registration:
categories.append(self.program.open_class_category)
category_filter = SearchFilter(
name='category', title='the category',
inputs=[SelectInput(field_name='category', english_name='',
options={str(cat.id): cat.category
for cat in categories})])

status_filter = SearchFilter(
name='status', title='the status',
inputs=[SelectInput(field_name='status', english_name='', options={
str(k): v for k, v in STATUS_CHOICES})])
title_filter = SearchFilter(
name='title', title='title containing',
inputs=[TextInput(field_name='title__icontains', english_name='')])
username_filter = SearchFilter(
name='username', title='teacher username containing',
inputs=[TextInput(field_name='teachers__username__contains',
english_name='')])
all_scheduled_filter = SearchFilter(
name="all_scheduled", title="all sections scheduled",
# Exclude classes with sections with null meeting times
inverted=True,
inputs=[ConstantInput(Q(sections__meeting_times__isnull=True))],
)
some_scheduled_filter = SearchFilter(
name="some_scheduled", title="some sections scheduled",
# Get classes with sections with non-null meeting times
inputs=[ConstantInput(Q(sections__meeting_times__isnull=False))],
)

return QueryBuilder(
base=ClassSubject.objects.filter(parent_program=self.program),
english_name="classes",
filters=[
status_filter,
category_filter,
title_filter,
username_filter,
flag_filter,
any_flag_filter,
all_scheduled_filter,
some_scheduled_filter,
])

@main_call
@needs_admin
def classsearch(self, request, tl, one, two, module, extra, prog):
data = request.GET.get('query')
query_builder = self.query_builder()
if data is None:
# Display a blank query builder
context = {
'query_builder': query_builder,
'program': self.program,
}
return render_to_response(self.baseDir()+'query_builder.html',
request, context)
else:
decoded = json.loads(data)
queryset = query_builder.as_queryset(decoded)
queryset = queryset.distinct().order_by('id').prefetch_related(
'flags', 'flags__flag_type', 'teachers', 'category',
'sections')
english = query_builder.as_english(decoded)
context = {
'queryset': queryset,
'english': english,
'program': self.program,
}
return render_to_response(self.baseDir()+'search_results.html',
request, context)
1 change: 1 addition & 0 deletions esp/esp/program/modules/tests/__init__.py
Expand Up @@ -47,3 +47,4 @@
from esp.program.modules.tests.resourcemodule import ResourceModuleTest
from esp.program.modules.tests.admincore import RegistrationTypeManagementTest
from esp.program.modules.tests.adminclass import CancelClassTest
from esp.program.modules.tests.classsearchmodule import ClassSearchModuleTest

0 comments on commit 685c76a

Please sign in to comment.