Merge lp:~canonical-ca-hackers/ubuntu-recommender/944060-tracking-user-actions into lp:ubuntu-recommender
- 944060-tracking-user-actions
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Łukasz Czyżykowski |
Approved revision: | 66 |
Merged at revision: | 61 |
Proposed branch: | lp:~canonical-ca-hackers/ubuntu-recommender/944060-tracking-user-actions |
Merge into: | lp:ubuntu-recommender |
Diff against target: |
388 lines (+275/-1) 6 files modified
src/recommender/api/forms.py (+9/-1) src/recommender/api/handlers.py (+22/-0) src/recommender/api/urls.py (+5/-0) src/recommender/migrations/0016_add_implicitfeedback.py (+163/-0) src/recommender/models/recommendations.py (+10/-0) src/recommender/tests/test_api.py (+66/-0) |
To merge this branch: | bzr merge lp:~canonical-ca-hackers/ubuntu-recommender/944060-tracking-user-actions |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Anthony Lenton (community) | Approve | ||
Review via email: mp+120170@code.launchpad.net |
Commit message
Added implicit_feedback api call.
Description of the change
Overview
========
Added implicit_feedback url for gathering user actions. Together with it there's a model where all that info is stored.
ISD Branch Mangler (isd-branches-mangler) wrote : | # |
The attempt to merge lp:~canonical-ca-hackers/ubuntu-recommender/944060-tracking-user-actions into lp:ubuntu-recommender failed. Below is the output from the failed tests.
New python executable in virtualenv/
Installing distribute.
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package configglue
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package coverage
no previously-included directories found matching 'test'
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-configglue
warning: no previously-included files matching '*.pyc' found anywhere in distribution
Obtaining django-pgtools from bzr+http://
Checking out http://
Running setup.py egg_info for package django-pgtools
Obtaining django-openid-auth from bzr+http://
Checking out http://
Running setup.py egg_info for package django-openid-auth
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-piston
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-preflight
warning: no files found matching 'templates/
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for...
ISD Branch Mangler (isd-branches-mangler) wrote : | # |
The attempt to merge lp:~canonical-ca-hackers/ubuntu-recommender/944060-tracking-user-actions into lp:ubuntu-recommender failed. Below is the output from the failed tests.
New python executable in virtualenv/
Installing distribute.
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package configglue
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package coverage
no previously-included directories found matching 'test'
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-configglue
warning: no previously-included files matching '*.pyc' found anywhere in distribution
Obtaining django-pgtools from bzr+http://
Checking out http://
Running setup.py egg_info for package django-pgtools
Obtaining django-openid-auth from bzr+http://
Checking out http://
Running setup.py egg_info for package django-openid-auth
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-piston
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-preflight
warning: no files found matching 'templates/
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for...
ISD Branch Mangler (isd-branches-mangler) wrote : | # |
The attempt to merge lp:~canonical-ca-hackers/ubuntu-recommender/944060-tracking-user-actions into lp:ubuntu-recommender failed. Below is the output from the failed tests.
New python executable in virtualenv/
Installing distribute.
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package configglue
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package coverage
no previously-included directories found matching 'test'
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-configglue
warning: no previously-included files matching '*.pyc' found anywhere in distribution
Obtaining django-pgtools from bzr+http://
Checking out http://
Running setup.py egg_info for package django-pgtools
Obtaining django-openid-auth from bzr+http://
Checking out http://
Running setup.py egg_info for package django-openid-auth
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-piston
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-preflight
warning: no files found matching 'templates/
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for...
ISD Branch Mangler (isd-branches-mangler) wrote : | # |
The attempt to merge lp:~canonical-ca-hackers/ubuntu-recommender/944060-tracking-user-actions into lp:ubuntu-recommender failed. Below is the output from the failed tests.
New python executable in virtualenv/
Installing distribute.
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package configglue
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package coverage
no previously-included directories found matching 'test'
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-configglue
warning: no previously-included files matching '*.pyc' found anywhere in distribution
Obtaining django-pgtools from bzr+http://
Checking out http://
Running setup.py egg_info for package django-pgtools
Obtaining django-openid-auth from bzr+http://
Checking out http://
Running setup.py egg_info for package django-openid-auth
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-piston
Downloading/
Using download cache from /home/ubuntu/
Running setup.py egg_info for package django-preflight
warning: no files found matching 'templates/
Downloading/
Storing download in cache at /home/ubuntu/
Running setup.py egg_info ...
Preview Diff
1 | === modified file 'src/recommender/api/forms.py' |
2 | --- src/recommender/api/forms.py 2012-06-22 19:55:46 +0000 |
3 | +++ src/recommender/api/forms.py 2012-08-20 14:19:20 +0000 |
4 | @@ -22,11 +22,13 @@ |
5 | __metaclass__ = type |
6 | __all__ = [ |
7 | 'RecommendationFeedbackForm', |
8 | + 'RemoveAppForm', |
9 | + 'ImplicitFeedbackForm', |
10 | ] |
11 | |
12 | from django import forms |
13 | |
14 | -from recommender.models import UserRecommendation |
15 | +from recommender.models import UserRecommendation, ImplicitFeedback |
16 | |
17 | |
18 | class RecommendationFeedbackForm(forms.ModelForm): |
19 | @@ -41,3 +43,9 @@ |
20 | class RemoveAppForm(forms.Form): |
21 | app = forms.CharField(max_length=64) |
22 | remove = forms.BooleanField(required=False) |
23 | + |
24 | + |
25 | +class ImplicitFeedbackForm(forms.ModelForm): |
26 | + class Meta: |
27 | + model = ImplicitFeedback |
28 | + fields = ('package_name', 'action') |
29 | |
30 | === modified file 'src/recommender/api/handlers.py' |
31 | --- src/recommender/api/handlers.py 2012-06-22 19:55:46 +0000 |
32 | +++ src/recommender/api/handlers.py 2012-08-20 14:19:20 +0000 |
33 | @@ -25,6 +25,8 @@ |
34 | 'ProfileHandler', |
35 | 'MeHandler', |
36 | 'AppHandler', |
37 | + 'FeedbackHandler', |
38 | + 'ImplicitFeedbackHandler', |
39 | ] |
40 | |
41 | import json |
42 | @@ -48,6 +50,7 @@ |
43 | from .forms import ( |
44 | RecommendationFeedbackForm, |
45 | RemoveAppForm, |
46 | + ImplicitFeedbackForm, |
47 | ) |
48 | |
49 | |
50 | @@ -184,6 +187,25 @@ |
51 | return 'ok' |
52 | |
53 | |
54 | +class ImplicitFeedbackHandler(BaseHandler): |
55 | + allowed_methods = ('POST',) |
56 | + |
57 | + def create(self, request): |
58 | + if not hasattr(request, 'data'): |
59 | + return HttpResponseBadRequest('Invalid data submitted') |
60 | + |
61 | + form = ImplicitFeedbackForm(request.data) |
62 | + if not form.is_valid(): |
63 | + result = serialize_form_errors(form) |
64 | + return HttpResponseBadRequest(result) |
65 | + |
66 | + feedback = form.save(commit=False) |
67 | + feedback.user = request.user |
68 | + feedback.save() |
69 | + |
70 | + return 'ok' |
71 | + |
72 | + |
73 | class RemoveHandler(BaseHandler): |
74 | allowed_methods = ('POST',) |
75 | |
76 | |
77 | === modified file 'src/recommender/api/urls.py' |
78 | --- src/recommender/api/urls.py 2012-06-22 19:55:46 +0000 |
79 | +++ src/recommender/api/urls.py 2012-08-20 14:19:20 +0000 |
80 | @@ -39,6 +39,7 @@ |
81 | from .handlers import ( |
82 | AppHandler, |
83 | FeedbackHandler, |
84 | + ImplicitFeedbackHandler, |
85 | MeHandler, |
86 | ProfileHandler, |
87 | RemoveHandler, |
88 | @@ -69,6 +70,8 @@ |
89 | top_resource = dont_vary(cached_resource(Resource(handler=TopHandler))) |
90 | feedback_resource = never_cache(Resource(handler=FeedbackHandler, |
91 | authentication=auth)) |
92 | +implicit_feedback_resource = never_cache( |
93 | + Resource(handler=ImplicitFeedbackHandler, authentication=auth)) |
94 | remove_resource = never_cache(Resource(handler=RemoveHandler, |
95 | authentication=auth)) |
96 | |
97 | @@ -89,5 +92,7 @@ |
98 | url(r'^recommend_top/$', top_resource, |
99 | name='api-recommend-top'), |
100 | url(r'^feedback/$', feedback_resource, name='api-feedback'), |
101 | + url(r'^implicit_feedback/$', implicit_feedback_resource, |
102 | + name='api-feedback-implicit'), |
103 | url(r'^remove_app/$', remove_resource, name='api-remove-app'), |
104 | ) |
105 | |
106 | === added file 'src/recommender/migrations/0016_add_implicitfeedback.py' |
107 | --- src/recommender/migrations/0016_add_implicitfeedback.py 1970-01-01 00:00:00 +0000 |
108 | +++ src/recommender/migrations/0016_add_implicitfeedback.py 2012-08-20 14:19:20 +0000 |
109 | @@ -0,0 +1,163 @@ |
110 | +# encoding: utf-8 |
111 | +from south.db import db |
112 | +from south.v2 import SchemaMigration |
113 | + |
114 | + |
115 | +class Migration(SchemaMigration): |
116 | + |
117 | + def forwards(self, orm): |
118 | + db.create_table('recommender_implicitfeedback', ( |
119 | + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), |
120 | + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), |
121 | + ('package_name', self.gf('django.db.models.fields.CharField')(max_length=255)), |
122 | + ('action', self.gf('django.db.models.fields.CharField')(max_length=20)), |
123 | + )) |
124 | + db.send_create_signal('recommender', ['ImplicitFeedback']) |
125 | + |
126 | + def backwards(self, orm): |
127 | + db.delete_table('recommender_implicitfeedback') |
128 | + |
129 | + models = { |
130 | + 'auth.group': { |
131 | + 'Meta': {'object_name': 'Group'}, |
132 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
133 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), |
134 | + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) |
135 | + }, |
136 | + 'auth.permission': { |
137 | + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, |
138 | + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
139 | + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), |
140 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
141 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) |
142 | + }, |
143 | + 'auth.user': { |
144 | + 'Meta': {'object_name': 'User'}, |
145 | + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
146 | + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), |
147 | + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
148 | + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), |
149 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
150 | + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
151 | + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
152 | + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
153 | + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
154 | + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
155 | + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), |
156 | + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), |
157 | + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) |
158 | + }, |
159 | + 'contenttypes.contenttype': { |
160 | + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, |
161 | + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
162 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
163 | + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
164 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
165 | + }, |
166 | + 'recommender.apprecommendation': { |
167 | + 'Meta': {'object_name': 'AppRecommendation'}, |
168 | + 'data': ('django.db.models.fields.TextField', [], {}), |
169 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), |
170 | + 'feedback': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
171 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
172 | + 'package': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['recommender.Package']"}), |
173 | + 'rid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}) |
174 | + }, |
175 | + 'recommender.consumer': { |
176 | + 'Meta': {'object_name': 'Consumer'}, |
177 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
178 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
179 | + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), |
180 | + 'secret': ('django.db.models.fields.CharField', [], {'default': "'MPwSyovHBlpbsiAutqLwyXzsFroEfJ'", 'max_length': '255', 'blank': 'True'}), |
181 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), |
182 | + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"}) |
183 | + }, |
184 | + 'recommender.implicitfeedback': { |
185 | + 'Meta': {'object_name': 'ImplicitFeedback'}, |
186 | + 'action': ('django.db.models.fields.CharField', [], {'max_length': '20'}), |
187 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
188 | + 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
189 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) |
190 | + }, |
191 | + 'recommender.machineprofile': { |
192 | + 'Meta': {'object_name': 'MachineProfile'}, |
193 | + 'data': ('django.db.models.fields.TextField', [], {}), |
194 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
195 | + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) |
196 | + }, |
197 | + 'recommender.nonce': { |
198 | + 'Meta': {'object_name': 'Nonce'}, |
199 | + 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['recommender.Consumer']"}), |
200 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
201 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
202 | + 'nonce': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), |
203 | + 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['recommender.Token']"}) |
204 | + }, |
205 | + 'recommender.package': { |
206 | + 'Meta': {'unique_together': "(('name', 'app_name'),)", 'object_name': 'Package'}, |
207 | + 'app_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
208 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
209 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), |
210 | + 'wilson_score': ('django.db.models.fields.FloatField', [], {'default': '0.0'}) |
211 | + }, |
212 | + 'recommender.personidentification': { |
213 | + 'Meta': {'object_name': 'PersonIdentification'}, |
214 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
215 | + 'rnr_username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), |
216 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True'}) |
217 | + }, |
218 | + 'recommender.ratingsandreviewsdataitem': { |
219 | + 'Meta': {'ordering': "('date_created',)", 'unique_together': "(('package', 'person_identification'),)", 'object_name': 'RatingsAndReviewsDataItem'}, |
220 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {}), |
221 | + 'distroseries': ('django.db.models.fields.CharField', [], {'max_length': '50'}), |
222 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
223 | + 'language': ('django.db.models.fields.CharField', [], {'max_length': '10'}), |
224 | + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '50'}), |
225 | + 'package': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['recommender.Package']", 'null': 'True'}), |
226 | + 'person_identification': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['recommender.PersonIdentification']"}), |
227 | + 'rating': ('django.db.models.fields.IntegerField', [], {}), |
228 | + 'review_text': ('django.db.models.fields.TextField', [], {}), |
229 | + 'slope_one_used': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
230 | + 'version': ('django.db.models.fields.CharField', [], {'max_length': '255'}) |
231 | + }, |
232 | + 'recommender.removedapp': { |
233 | + 'Meta': {'object_name': 'RemovedApp'}, |
234 | + 'app': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['recommender.Package']"}), |
235 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
236 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) |
237 | + }, |
238 | + 'recommender.slopeonefrequency': { |
239 | + 'Meta': {'object_name': 'SlopeOneFrequency'}, |
240 | + 'difference': ('django.db.models.fields.FloatField', [], {}), |
241 | + 'frequency': ('django.db.models.fields.IntegerField', [], {}), |
242 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
243 | + 'package_1': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['recommender.Package']"}), |
244 | + 'package_2': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['recommender.Package']"}) |
245 | + }, |
246 | + 'recommender.token': { |
247 | + 'Meta': {'object_name': 'Token'}, |
248 | + 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['recommender.Consumer']"}), |
249 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
250 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
251 | + 'token': ('django.db.models.fields.CharField', [], {'default': "'KWBATTYcabhHCRPupLeaPxRTzfFIzdiSrEYjNtEGkYfMTnwQoC'", 'max_length': '50', 'primary_key': 'True'}), |
252 | + 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'rwzMGwfoyQSfUeQcrwDwwTALbWpddIVmgiAHCfTdQyQjkZwikS'", 'max_length': '50'}), |
253 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) |
254 | + }, |
255 | + 'recommender.userprofile': { |
256 | + 'Meta': {'object_name': 'UserProfile'}, |
257 | + 'data': ('django.db.models.fields.TextField', [], {}), |
258 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
259 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) |
260 | + }, |
261 | + 'recommender.userrecommendation': { |
262 | + 'Meta': {'object_name': 'UserRecommendation'}, |
263 | + 'data': ('django.db.models.fields.TextField', [], {}), |
264 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), |
265 | + 'feedback': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
266 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
267 | + 'rid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), |
268 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) |
269 | + } |
270 | + } |
271 | + |
272 | + complete_apps = ['recommender'] |
273 | |
274 | === modified file 'src/recommender/models/recommendations.py' |
275 | --- src/recommender/models/recommendations.py 2012-06-22 19:55:46 +0000 |
276 | +++ src/recommender/models/recommendations.py 2012-08-20 14:19:20 +0000 |
277 | @@ -27,6 +27,7 @@ |
278 | 'RemovedApp', |
279 | 'UserProfile', |
280 | 'UserRecommendation', |
281 | + 'ImplicitFeedback', |
282 | ] |
283 | |
284 | from datetime import datetime |
285 | @@ -83,3 +84,12 @@ |
286 | |
287 | class Meta: |
288 | app_label = 'recommender' |
289 | + |
290 | + |
291 | +class ImplicitFeedback(models.Model): |
292 | + user = models.ForeignKey(User) |
293 | + package_name = models.CharField(max_length=255) |
294 | + action = models.CharField(max_length=20) |
295 | + |
296 | + class Meta: |
297 | + app_label = 'recommender' |
298 | |
299 | === modified file 'src/recommender/tests/test_api.py' |
300 | --- src/recommender/tests/test_api.py 2012-06-22 19:55:46 +0000 |
301 | +++ src/recommender/tests/test_api.py 2012-08-20 14:19:20 +0000 |
302 | @@ -26,6 +26,7 @@ |
303 | 'ProfileTestCase', |
304 | 'RecommendAppTestCase', |
305 | 'RecommendationFeedbackTestCase', |
306 | + 'ImplicitFeedbackTestCase', |
307 | 'ObsoleteRecommendMeTestCase', |
308 | 'RecommendMeTestCase', |
309 | 'RecommendTopTestCase', |
310 | @@ -52,6 +53,7 @@ |
311 | Token, |
312 | UserProfile, |
313 | UserRecommendation, |
314 | + ImplicitFeedback, |
315 | ) |
316 | from .helpers import ( |
317 | create_oauth_token_and_consumer, |
318 | @@ -510,6 +512,70 @@ |
319 | self.assertEqual('"ok"', response.content) |
320 | |
321 | |
322 | +class ImplicitFeedbackTestCase(TestCase): |
323 | + |
324 | + def setUp(self): |
325 | + super(ImplicitFeedbackTestCase, self).setUp() |
326 | + self.token, self.consumer = create_oauth_token_and_consumer( |
327 | + self.factory) |
328 | + self.url = reverse('api-feedback-implicit') |
329 | + self.header = header_from_token(self.url, self.token, self.consumer) |
330 | + |
331 | + def test_implicit_feedback_isnt_anonymous(self): |
332 | + response = self.client.post(self.url) |
333 | + self.assertEqual(401, response.status_code) |
334 | + |
335 | + def test_implicit_feedback_requires_json_content_type(self): |
336 | + response = self.client.post( |
337 | + self.url, data={'id': 'foobar'}, **self.header) |
338 | + self.assertEqual('Invalid data submitted', response.content) |
339 | + self.assertEqual(400, response.status_code) |
340 | + |
341 | + def test_implicit_feedback_requires_valid_json(self): |
342 | + response = self.client.post( |
343 | + self.url, data='foobar', |
344 | + content_type='application/json', **self.header) |
345 | + self.assertEqual('Bad Request', response.content) |
346 | + self.assertEqual(400, response.status_code) |
347 | + |
348 | + def test_implicit_feedback_requires_valid_args(self): |
349 | + response = self.client.post( |
350 | + self.url, data='{"action": "b"}', |
351 | + content_type='application/json', **self.header) |
352 | + self.assertEqual( |
353 | + '{"package_name": ["This field is required."]}', |
354 | + response.content) |
355 | + self.assertEqual(400, response.status_code) |
356 | + |
357 | + def test_implicit_feedback_success(self): |
358 | + data = json.dumps({ |
359 | + 'package_name': "foobar", |
360 | + 'action': "installed", |
361 | + }) |
362 | + response = self.client.post( |
363 | + self.url, data=data, content_type='application/json', |
364 | + **self.header) |
365 | + self.assertEqual(200, response.status_code) |
366 | + self.assertEqual('"ok"', response.content) |
367 | + |
368 | + def test_implicit_feedback_sets_user_on_the_model(self): |
369 | + data = json.dumps({ |
370 | + 'package_name': "foobar", |
371 | + 'action': "installed", |
372 | + }) |
373 | + self.client.post( |
374 | + self.url, data=data, content_type='application/json', |
375 | + **self.header) |
376 | + self.assertEqual( |
377 | + 1, |
378 | + ImplicitFeedback.objects.filter( |
379 | + user=self.consumer.user, |
380 | + package_name="foobar", |
381 | + action="installed" |
382 | + ).count() |
383 | + ) |
384 | + |
385 | + |
386 | class RemoveAppTestCase(TestCase): |
387 | def setUp(self): |
388 | super(RemoveAppTestCase, self).setUp() |
Awesome Łukasz :D