First attempts at a get_favored_results method to find what people are generally...
[philo.git] / contrib / sobol / models.py
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
13 import datetime
14 try:
15         import eventlet
16 except:
17         eventlet = False
18
19
20 class Search(models.Model):
21         string = models.TextField()
22         
23         def __unicode__(self):
24                 return self.string
25         
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
29                 items."""
30                 results = self.result_urls.values_list('pk', 'url',)
31                 
32                 result_dict = {}
33                 for pk, url in results:
34                         result_dict[pk] = {'url': url, 'value': 0}
35                 
36                 clicks = Click.objects.filter(result__pk__in=result_dict.keys()).values_list('result__pk', 'datetime')
37                 
38                 now = datetime.datetime.now()
39                 
40                 def datetime_value(dt):
41                         days = (now - dt).days
42                         if days < 0:
43                                 raise ValueError("Click dates must be in the past.")
44                         if days == 0:
45                                 value = 1.0
46                         else:
47                                 value = 1.0/days**2
48                         return value
49                 
50                 for pk, dt in clicks:
51                         value = datetime_value(dt)
52                         result_dict[pk]['value'] += value
53                 
54                 #TODO: is there a reasonable minimum value for consideration?
55                 subsets = {}
56                 for d in result_dict.values():
57                         subsets.setdefault(d['value'], []).append(d)
58                 
59                 # Now calculate the result set.
60                 results = []
61                 
62                 def cost(value):
63                         return error*sum([(value - item['value'])**2 for item in results])
64                 
65                 for value, subset in sorted(subsets.items(), cmp=lambda x,y: cmp(y[0], x[0])):
66                         if value > cost(value):
67                                 results += subset
68                         else:
69                                 break
70                 return results
71         
72         class Meta:
73                 ordering = ['string']
74                 verbose_name_plural = 'searches'
75
76
77 class ResultURL(models.Model):
78         search = models.ForeignKey(Search, related_name='result_urls')
79         url = models.TextField(validators=[RedirectValidator()])
80         
81         def __unicode__(self):
82                 return self.url
83         
84         class Meta:
85                 ordering = ['url']
86
87
88 class Click(models.Model):
89         result = models.ForeignKey(ResultURL, related_name='clicks')
90         datetime = models.DateTimeField()
91         
92         def __unicode__(self):
93                 return self.datetime.strftime('%B %d, %Y %H:%M:%S')
94         
95         class Meta:
96                 ordering = ['datetime']
97                 get_latest_by = 'datetime'
98
99
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")
105         
106         def __unicode__(self):
107                 return u"%s (%s)" % (self.placeholder_text, u", ".join([display for slug, display in registry.iterchoices()]))
108         
109         def get_reverse_params(self, obj):
110                 raise ViewCanNotProvideSubpath
111         
112         @property
113         def urlpatterns(self):
114                 urlpatterns = patterns('',
115                         url(r'^$', self.results_view, name='results'),
116                 )
117                 if self.enable_ajax_api:
118                         urlpatterns += patterns('',
119                                 url(r'^(?P<slug>[\w-]+)', self.ajax_api_view, name='ajax_api_view')
120                         )
121                 return urlpatterns
122         
123         def get_search_instance(self, slug, search_string):
124                 return registry[slug](search_string.lower())
125         
126         def results_view(self, request, extra_context=None):
127                 results = None
128                 
129                 context = self.get_context()
130                 context.update(extra_context or {})
131                 
132                 if SEARCH_ARG_GET_KEY in request.GET:
133                         form = SearchForm(request.GET)
134                         
135                         if form.is_valid():
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)
139                                 
140                                 if url and hash:
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)
147                                         else:
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 = []
153                                         if eventlet:
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)
158                                                 if eventlet:
159                                                         pool.spawn_n(self.make_result_cache, search_instance)
160                                                 else:
161                                                         self.make_result_cache(search_instance)
162                                         if eventlet:
163                                                 pool.waitall()
164                                         context.update({
165                                                 'searches': search_instances
166                                         })
167                 else:
168                         form = SearchForm()
169                 
170                 context.update({
171                         'form': form
172                 })
173                 return self.results_page.render_to_response(request, extra_context=context)
174         
175         def make_result_cache(self, search_instance):
176                 search_instance.results
177         
178         def ajax_api_view(self, request, slug, extra_context=None):
179                 search_string = request.GET.get(SEARCH_ARG_GET_KEY)
180                 
181                 if not request.is_ajax() or not self.enable_ajax_api or slug not in self.searches or search_string is None:
182                         raise Http404
183                 
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()
188                 })
189                 return response