1 from django.conf.urls.defaults import patterns, url
2 from django.contrib import messages
3 from django.core.exceptions import ValidationError
4 from django.db import models
5 from django.http import HttpResponseRedirect, Http404, HttpResponse
6 from django.utils import simplejson as json
7 from django.utils.datastructures import SortedDict
8 from philo.contrib.sobol import registry
9 from philo.contrib.sobol.forms import SearchForm
10 from philo.contrib.sobol.utils import HASH_REDIRECT_GET_KEY, URL_REDIRECT_GET_KEY, SEARCH_ARG_GET_KEY, check_redirect_hash
11 from philo.exceptions import ViewCanNotProvideSubpath
12 from philo.models import MultiView, Page
13 from philo.models.fields import SlugMultipleChoiceField
14 from philo.validators import RedirectValidator
22 class Search(models.Model):
23 string = models.TextField()
25 def __unicode__(self):
28 def get_weighted_results(self, threshhold=None):
29 "Returns this search's results ordered by decreasing weight."
30 if not hasattr(self, '_weighted_results'):
31 result_qs = self.result_urls.all()
33 if threshhold is not None:
34 result_qs = result_qs.filter(counts__datetime__gte=threshhold)
36 results = [result for result in result_qs]
38 results.sort(cmp=lambda x,y: cmp(y.weight, x.weight))
40 self._weighted_results = results
42 return self._weighted_results
44 def get_favored_results(self, error=5, threshhold=None):
46 Calculate the set of most-favored results. A higher error
47 will cause this method to be more reticent about adding new
50 The thought is to see whether there are any results which
51 vastly outstrip the other options. As such, evenly-weighted
52 results should be grouped together and either added or
55 if not hasattr(self, '_favored_results'):
56 results = self.get_weighted_results(threshhold)
58 grouped_results = SortedDict()
60 for result in results:
61 grouped_results.setdefault(result.weight, []).append(result)
63 self._favored_results = []
65 for value, subresults in grouped_results.items():
66 cost = error * sum([(value - result.weight)**2 for result in self._favored_results])
68 self._favored_results += subresults
71 return self._favored_results
75 verbose_name_plural = 'searches'
78 class ResultURL(models.Model):
79 search = models.ForeignKey(Search, related_name='result_urls')
80 url = models.TextField(validators=[RedirectValidator()])
82 def __unicode__(self):
85 def get_weight(self, threshhold=None):
86 if not hasattr(self, '_weight'):
87 clicks = self.clicks.all()
89 if threshhold is not None:
90 clicks = clicks.filter(datetime__gte=threshhold)
92 self._weight = sum([click.weight for click in clicks])
95 weight = property(get_weight)
101 class Click(models.Model):
102 result = models.ForeignKey(ResultURL, related_name='clicks')
103 datetime = models.DateTimeField()
105 def __unicode__(self):
106 return self.datetime.strftime('%B %d, %Y %H:%M:%S')
108 def get_weight(self, default=1, weighted=lambda value, days: value/days**2):
109 if not hasattr(self, '_weight'):
110 days = (datetime.datetime.now() - self.datetime).days
112 raise ValueError("Click dates must be in the past.")
113 default = float(default)
115 self._weight = float(default)
117 self._weight = weighted(default, days)
119 weight = property(get_weight)
122 if self.datetime > datetime.datetime.now():
123 raise ValidationError("Click dates must be in the past.")
126 ordering = ['datetime']
127 get_latest_by = 'datetime'
130 class SearchView(MultiView):
131 results_page = models.ForeignKey(Page, related_name='search_results_related')
132 searches = SlugMultipleChoiceField(choices=registry.iterchoices())
133 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.")
134 placeholder_text = models.CharField(max_length=75, default="Search")
136 search_form = SearchForm
138 def __unicode__(self):
139 return u"%s (%s)" % (self.placeholder_text, u", ".join([display for slug, display in registry.iterchoices() if slug in self.searches]))
141 def get_reverse_params(self, obj):
142 raise ViewCanNotProvideSubpath
145 def urlpatterns(self):
146 urlpatterns = patterns('',
147 url(r'^$', self.results_view, name='results'),
149 if self.enable_ajax_api:
150 urlpatterns += patterns('',
151 url(r'^(?P<slug>[\w-]+)$', self.ajax_api_view, name='ajax_api_view')
155 def get_search_instance(self, slug, search_string):
156 return registry[slug](search_string.lower())
158 def results_view(self, request, extra_context=None):
161 context = self.get_context()
162 context.update(extra_context or {})
164 if SEARCH_ARG_GET_KEY in request.GET:
165 form = self.search_form(request.GET)
168 search_string = request.GET[SEARCH_ARG_GET_KEY].lower()
169 url = request.GET.get(URL_REDIRECT_GET_KEY)
170 hash = request.GET.get(HASH_REDIRECT_GET_KEY)
173 if check_redirect_hash(hash, search_string, url):
174 # Create the necessary models
175 search = Search.objects.get_or_create(string=search_string)[0]
176 result_url = search.result_urls.get_or_create(url=url)[0]
177 result_url.clicks.create(datetime=datetime.datetime.now())
178 return HttpResponseRedirect(url)
180 messages.add_message(request, messages.INFO, "The link you followed had been tampered with. Here are all the results for your search term instead!")
181 # TODO: Should search_string be escaped here?
182 return HttpResponseRedirect("%s?%s=%s" % (request.path, SEARCH_ARG_GET_KEY, search_string))
183 if not self.enable_ajax_api:
184 search_instances = []
186 pool = eventlet.GreenPool()
187 for slug in self.searches:
188 search_instance = self.get_search_instance(slug, search_string)
189 search_instances.append(search_instance)
191 pool.spawn_n(self.make_result_cache, search_instance)
193 self.make_result_cache(search_instance)
197 'searches': search_instances
201 'searches': [{'verbose_name': verbose_name, 'url': self.reverse('ajax_api_view', kwargs={'slug': slug}, node=request.node)} for slug, verbose_name in registry.iterchoices() if slug in self.searches]
209 return self.results_page.render_to_response(request, extra_context=context)
211 def make_result_cache(self, search_instance):
212 search_instance.results
214 def ajax_api_view(self, request, slug, extra_context=None):
215 search_string = request.GET.get(SEARCH_ARG_GET_KEY)
217 if not request.is_ajax() or not self.enable_ajax_api or slug not in self.searches or search_string is None:
220 search_instance = self.get_search_instance(slug, search_string)
221 response = HttpResponse(json.dumps({
222 'results': [result.get_context() for result in search_instance.results],