1 from django.conf.urls.defaults import patterns, url
2 from django.contrib import messages
3 from django.db import models
4 from django.http import HttpResponseRedirect, Http404
5 from django.utils import simplejson as json
6 from philo.contrib.sobol import registry
7 from philo.contrib.sobol.forms import SearchForm
8 from philo.contrib.sobol.utils import HASH_REDIRECT_GET_KEY, URL_REDIRECT_GET_KEY, SEARCH_ARG_GET_KEY, check_redirect_hash
9 from philo.exceptions import ViewCanNotProvideSubpath
10 from philo.models import MultiView, Page
11 from philo.models.fields import SlugMultipleChoiceField
12 from philo.validators import RedirectValidator
20 class Search(models.Model):
21 string = models.TextField()
23 def __unicode__(self):
26 def get_favored_results(self, error=5):
27 """Calculate the set of most-favored results. A higher error
28 will cause this method to be more reticent about adding new
30 results = self.result_urls.values_list('pk', 'url',)
33 for pk, url in results:
34 result_dict[pk] = {'url': url, 'value': 0}
36 clicks = Click.objects.filter(result__pk__in=result_dict.keys()).values_list('result__pk', 'datetime')
38 now = datetime.datetime.now()
40 def datetime_value(dt):
41 days = (now - dt).days
43 raise ValueError("Click dates must be in the past.")
51 value = datetime_value(dt)
52 result_dict[pk]['value'] += value
54 #TODO: is there a reasonable minimum value for consideration?
56 for d in result_dict.values():
57 subsets.setdefault(d['value'], []).append(d)
59 # Now calculate the result set.
63 return error*sum([(value - item['value'])**2 for item in results])
65 for value, subset in sorted(subsets.items(), cmp=lambda x,y: cmp(y[0], x[0])):
66 if value > cost(value):
74 verbose_name_plural = 'searches'
77 class ResultURL(models.Model):
78 search = models.ForeignKey(Search, related_name='result_urls')
79 url = models.TextField(validators=[RedirectValidator()])
81 def __unicode__(self):
88 class Click(models.Model):
89 result = models.ForeignKey(ResultURL, related_name='clicks')
90 datetime = models.DateTimeField()
92 def __unicode__(self):
93 return self.datetime.strftime('%B %d, %Y %H:%M:%S')
96 ordering = ['datetime']
97 get_latest_by = 'datetime'
100 class SearchView(MultiView):
101 results_page = models.ForeignKey(Page, related_name='search_results_related')
102 searches = SlugMultipleChoiceField(choices=registry.iterchoices())
103 enable_ajax_api = models.BooleanField("Enable AJAX API", default=True)
104 placeholder_text = models.CharField(max_length=75, default="Search")
106 def __unicode__(self):
107 return u"%s (%s)" % (self.placeholder_text, u", ".join([display for slug, display in registry.iterchoices()]))
109 def get_reverse_params(self, obj):
110 raise ViewCanNotProvideSubpath
113 def urlpatterns(self):
114 urlpatterns = patterns('',
115 url(r'^$', self.results_view, name='results'),
117 if self.enable_ajax_api:
118 urlpatterns += patterns('',
119 url(r'^(?P<slug>[\w-]+)', self.ajax_api_view, name='ajax_api_view')
123 def get_search_instance(self, slug, search_string):
124 return registry[slug](search_string.lower())
126 def results_view(self, request, extra_context=None):
129 context = self.get_context()
130 context.update(extra_context or {})
132 if SEARCH_ARG_GET_KEY in request.GET:
133 form = SearchForm(request.GET)
136 search_string = request.GET[SEARCH_ARG_GET_KEY].lower()
137 url = request.GET.get(URL_REDIRECT_GET_KEY)
138 hash = request.GET.get(HASH_REDIRECT_GET_KEY)
141 if check_redirect_hash(hash, search_string, url):
142 # Create the necessary models
143 search = Search.objects.get_or_create(string=search_string)[0]
144 result_url = search.result_urls.get_or_create(url=url)[0]
145 result_url.clicks.create(datetime=datetime.datetime.now())
146 return HttpResponseRedirect(url)
148 messages.add_message(request, messages.INFO, "The link you followed had been tampered with. Here are all the results for your search term instead!")
149 # TODO: Should search_string be escaped here?
150 return HttpResponseRedirect("%s?%s=%s" % (request.path, SEARCH_ARG_GET_KEY, search_string))
151 if not self.enable_ajax_api:
152 search_instances = []
154 pool = eventlet.GreenPool()
155 for slug in self.searches:
156 search_instance = self.get_search_instance(slug, search_string)
157 search_instances.append(search_instance)
159 pool.spawn_n(self.make_result_cache, search_instance)
161 self.make_result_cache(search_instance)
165 'searches': search_instances
173 return self.results_page.render_to_response(request, extra_context=context)
175 def make_result_cache(self, search_instance):
176 search_instance.results
178 def ajax_api_view(self, request, slug, extra_context=None):
179 search_string = request.GET.get(SEARCH_ARG_GET_KEY)
181 if not request.is_ajax() or not self.enable_ajax_api or slug not in self.searches or search_string is None:
184 search_instance = self.get_search_instance(slug, search_string)
185 response = json.dumps({
186 'results': search_instance.results,
187 'template': search_instance.get_template()