3 from django.conf import settings
4 from django.conf.urls.defaults import patterns, url
5 from django.contrib import messages
6 from django.core.exceptions import ValidationError
7 from django.db import models
8 from django.http import HttpResponseRedirect, Http404, HttpResponse
9 from django.utils import simplejson as json
10 from django.utils.datastructures import SortedDict
12 from philo.contrib.sobol import registry
13 from philo.contrib.sobol.forms import SearchForm
14 from philo.contrib.sobol.utils import HASH_REDIRECT_GET_KEY, URL_REDIRECT_GET_KEY, SEARCH_ARG_GET_KEY, check_redirect_hash
15 from philo.exceptions import ViewCanNotProvideSubpath
16 from philo.models import MultiView, Page
17 from philo.models.fields import SlugMultipleChoiceField
18 from philo.validators import RedirectValidator
21 if getattr(settings, 'SOBOL_USE_EVENTLET', False):
28 class Search(models.Model):
29 string = models.TextField()
31 def __unicode__(self):
34 def get_weighted_results(self, threshhold=None):
35 "Returns this search's results ordered by decreasing weight."
36 if not hasattr(self, '_weighted_results'):
37 result_qs = self.result_urls.all()
39 if threshhold is not None:
40 result_qs = result_qs.filter(counts__datetime__gte=threshhold)
42 results = [result for result in result_qs]
44 results.sort(cmp=lambda x,y: cmp(y.weight, x.weight))
46 self._weighted_results = results
48 return self._weighted_results
50 def get_favored_results(self, error=5, threshhold=None):
52 Calculate the set of most-favored results. A higher error
53 will cause this method to be more reticent about adding new
56 The thought is to see whether there are any results which
57 vastly outstrip the other options. As such, evenly-weighted
58 results should be grouped together and either added or
61 if not hasattr(self, '_favored_results'):
62 results = self.get_weighted_results(threshhold)
64 grouped_results = SortedDict()
66 for result in results:
67 grouped_results.setdefault(result.weight, []).append(result)
69 self._favored_results = []
71 for value, subresults in grouped_results.items():
72 cost = error * sum([(value - result.weight)**2 for result in self._favored_results])
74 self._favored_results += subresults
77 return self._favored_results
81 verbose_name_plural = 'searches'
84 class ResultURL(models.Model):
85 search = models.ForeignKey(Search, related_name='result_urls')
86 url = models.TextField(validators=[RedirectValidator()])
88 def __unicode__(self):
91 def get_weight(self, threshhold=None):
92 if not hasattr(self, '_weight'):
93 clicks = self.clicks.all()
95 if threshhold is not None:
96 clicks = clicks.filter(datetime__gte=threshhold)
98 self._weight = sum([click.weight for click in clicks])
101 weight = property(get_weight)
107 class Click(models.Model):
108 result = models.ForeignKey(ResultURL, related_name='clicks')
109 datetime = models.DateTimeField()
111 def __unicode__(self):
112 return self.datetime.strftime('%B %d, %Y %H:%M:%S')
114 def get_weight(self, default=1, weighted=lambda value, days: value/days**2):
115 if not hasattr(self, '_weight'):
116 days = (datetime.datetime.now() - self.datetime).days
118 raise ValueError("Click dates must be in the past.")
119 default = float(default)
121 self._weight = float(default)
123 self._weight = weighted(default, days)
125 weight = property(get_weight)
128 if self.datetime > datetime.datetime.now():
129 raise ValidationError("Click dates must be in the past.")
132 ordering = ['datetime']
133 get_latest_by = 'datetime'
136 class SearchView(MultiView):
137 results_page = models.ForeignKey(Page, related_name='search_results_related')
138 searches = SlugMultipleChoiceField(choices=registry.iterchoices())
139 enable_ajax_api = models.BooleanField("Enable AJAX API", default=True, help_text="Search results will be available <i>only</i> by AJAX, not as template variables.")
140 placeholder_text = models.CharField(max_length=75, default="Search")
142 search_form = SearchForm
144 def __unicode__(self):
145 return u"%s (%s)" % (self.placeholder_text, u", ".join([display for slug, display in registry.iterchoices() if slug in self.searches]))
147 def get_reverse_params(self, obj):
148 raise ViewCanNotProvideSubpath
151 def urlpatterns(self):
152 urlpatterns = patterns('',
153 url(r'^$', self.results_view, name='results'),
155 if self.enable_ajax_api:
156 urlpatterns += patterns('',
157 url(r'^(?P<slug>[\w-]+)$', self.ajax_api_view, name='ajax_api_view')
161 def get_search_instance(self, slug, search_string):
162 return registry[slug](search_string.lower())
164 def results_view(self, request, extra_context=None):
167 context = self.get_context()
168 context.update(extra_context or {})
170 if SEARCH_ARG_GET_KEY in request.GET:
171 form = self.search_form(request.GET)
174 search_string = request.GET[SEARCH_ARG_GET_KEY].lower()
175 url = request.GET.get(URL_REDIRECT_GET_KEY)
176 hash = request.GET.get(HASH_REDIRECT_GET_KEY)
179 if check_redirect_hash(hash, search_string, url):
180 # Create the necessary models
181 search = Search.objects.get_or_create(string=search_string)[0]
182 result_url = search.result_urls.get_or_create(url=url)[0]
183 result_url.clicks.create(datetime=datetime.datetime.now())
184 return HttpResponseRedirect(url)
186 messages.add_message(request, messages.INFO, "The link you followed had been tampered with. Here are all the results for your search term instead!")
187 # TODO: Should search_string be escaped here?
188 return HttpResponseRedirect("%s?%s=%s" % (request.path, SEARCH_ARG_GET_KEY, search_string))
189 if not self.enable_ajax_api:
190 search_instances = []
192 pool = eventlet.GreenPool()
193 for slug in self.searches:
194 search_instance = self.get_search_instance(slug, search_string)
195 search_instances.append(search_instance)
197 pool.spawn_n(self.make_result_cache, search_instance)
199 self.make_result_cache(search_instance)
203 'searches': search_instances
207 'searches': [{'verbose_name': verbose_name, 'slug': slug, 'url': self.reverse('ajax_api_view', kwargs={'slug': slug}, node=request.node), 'result_template': registry[slug].result_template} for slug, verbose_name in registry.iterchoices() if slug in self.searches]
215 return self.results_page.render_to_response(request, extra_context=context)
217 def make_result_cache(self, search_instance):
218 search_instance.results
220 def ajax_api_view(self, request, slug, extra_context=None):
221 search_string = request.GET.get(SEARCH_ARG_GET_KEY)
223 if not request.is_ajax() or not self.enable_ajax_api or slug not in self.searches or search_string is None:
226 search_instance = self.get_search_instance(slug, search_string)
227 response = HttpResponse(json.dumps({
228 'results': [result.get_context() for result in search_instance.results],