Merge lp:~canonical-ca-hackers/ubuntu-recommender/944060-tracking-user-actions into lp:ubuntu-recommender

Proposed by Łukasz Czyżykowski on 2012-08-17
Status: Merged
Approved by: Łukasz Czyżykowski on 2012-08-20
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
Reviewer Review Type Date Requested Status
Anthony Lenton (community) 2012-08-17 Approve on 2012-08-17
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.

To post a comment you must log in.
Anthony Lenton (elachuni) wrote :

Awesome Łukasz :D

review: Approve
Download full text (51.2 KiB)

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/bin/python
Installing distribute..................................................................................................................................................................................done.
Downloading/unpacking configglue==1.0.1 (from -r requirements.txt (line 1))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fc%2Fconfigglue%2Fconfigglue-1.0.1.tar.gz
  Running setup.py egg_info for package configglue
Downloading/unpacking coverage (from -r requirements.txt (line 2))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fc%2Fcoverage%2Fcoverage-3.5.2.tar.gz
  Running setup.py egg_info for package coverage
    no previously-included directories found matching 'test'
Downloading/unpacking django==1.3 (from -r requirements.txt (line 3))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2FD%2FDjango%2FDjango-1.3.tar.gz
  Running setup.py egg_info for package django
Downloading/unpacking django-configglue==0.6.1 (from -r requirements.txt (line 4))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-configglue%2Fdjango-configglue-0.6.1.tar.gz
  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://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk (from -r requirements.txt (line 5))
  Checking out http://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk to ./virtualenv/src/django-pgtools
  Running setup.py egg_info for package django-pgtools
Obtaining django-openid-auth from bzr+http://bazaar.launchpad.net/~django-openid-auth/django-openid-auth/trunk (from -r requirements.txt (line 6))
  Checking out http://bazaar.launchpad.net/~django-openid-auth/django-openid-auth/trunk to ./virtualenv/src/django-openid-auth
  Running setup.py egg_info for package django-openid-auth
Downloading/unpacking django-piston==0.2.2.1 (from -r requirements.txt (line 7))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-piston%2Fdjango-piston-0.2.2.1.tar.gz
  Running setup.py egg_info for package django-piston
Downloading/unpacking django-preflight (from -r requirements.txt (line 8))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-preflight%2Fdjango-preflight-0.1.3.tar.gz
  Running setup.py egg_info for package django-preflight
    warning: no files found matching 'templates/preflight/*.html'
Downloading/unpacking django-sentry (from -r requirements.txt (line 9))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-sentry%2Fdjango-sentry-1.13.5.tar.gz
  Running setup.py egg_info for...

Download full text (51.1 KiB)

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/bin/python
Installing distribute..................................................................................................................................................................................done.
Downloading/unpacking configglue==1.0.1 (from -r requirements.txt (line 1))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fc%2Fconfigglue%2Fconfigglue-1.0.1.tar.gz
  Running setup.py egg_info for package configglue
Downloading/unpacking coverage (from -r requirements.txt (line 2))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fc%2Fcoverage%2Fcoverage-3.5.2.tar.gz
  Running setup.py egg_info for package coverage
    no previously-included directories found matching 'test'
Downloading/unpacking django==1.3 (from -r requirements.txt (line 3))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2FD%2FDjango%2FDjango-1.3.tar.gz
  Running setup.py egg_info for package django
Downloading/unpacking django-configglue==0.6.1 (from -r requirements.txt (line 4))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-configglue%2Fdjango-configglue-0.6.1.tar.gz
  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://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk (from -r requirements.txt (line 5))
  Checking out http://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk to ./virtualenv/src/django-pgtools
  Running setup.py egg_info for package django-pgtools
Obtaining django-openid-auth from bzr+http://bazaar.launchpad.net/~django-openid-auth/django-openid-auth/trunk (from -r requirements.txt (line 6))
  Checking out http://bazaar.launchpad.net/~django-openid-auth/django-openid-auth/trunk to ./virtualenv/src/django-openid-auth
  Running setup.py egg_info for package django-openid-auth
Downloading/unpacking django-piston==0.2.2.1 (from -r requirements.txt (line 7))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-piston%2Fdjango-piston-0.2.2.1.tar.gz
  Running setup.py egg_info for package django-piston
Downloading/unpacking django-preflight (from -r requirements.txt (line 8))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-preflight%2Fdjango-preflight-0.1.3.tar.gz
  Running setup.py egg_info for package django-preflight
    warning: no files found matching 'templates/preflight/*.html'
Downloading/unpacking django-sentry (from -r requirements.txt (line 9))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-sentry%2Fdjango-sentry-1.13.5.tar.gz
  Running setup.py egg_info for...

Download full text (49.0 KiB)

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/bin/python
Installing distribute..................................................................................................................................................................................done.
Downloading/unpacking configglue==1.0.1 (from -r requirements.txt (line 1))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fc%2Fconfigglue%2Fconfigglue-1.0.1.tar.gz
  Running setup.py egg_info for package configglue
Downloading/unpacking coverage (from -r requirements.txt (line 2))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fc%2Fcoverage%2Fcoverage-3.5.2.tar.gz
  Running setup.py egg_info for package coverage
    no previously-included directories found matching 'test'
Downloading/unpacking django==1.3 (from -r requirements.txt (line 3))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2FD%2FDjango%2FDjango-1.3.tar.gz
  Running setup.py egg_info for package django
Downloading/unpacking django-configglue==0.6.1 (from -r requirements.txt (line 4))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-configglue%2Fdjango-configglue-0.6.1.tar.gz
  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://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk (from -r requirements.txt (line 5))
  Checking out http://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk to ./virtualenv/src/django-pgtools
  Running setup.py egg_info for package django-pgtools
Obtaining django-openid-auth from bzr+http://bazaar.launchpad.net/~django-openid-auth/django-openid-auth/trunk (from -r requirements.txt (line 6))
  Checking out http://bazaar.launchpad.net/~django-openid-auth/django-openid-auth/trunk to ./virtualenv/src/django-openid-auth
  Running setup.py egg_info for package django-openid-auth
Downloading/unpacking django-piston==0.2.2.1 (from -r requirements.txt (line 7))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-piston%2Fdjango-piston-0.2.2.1.tar.gz
  Running setup.py egg_info for package django-piston
Downloading/unpacking django-preflight (from -r requirements.txt (line 8))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-preflight%2Fdjango-preflight-0.1.3.tar.gz
  Running setup.py egg_info for package django-preflight
    warning: no files found matching 'templates/preflight/*.html'
Downloading/unpacking django-sentry (from -r requirements.txt (line 9))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-sentry%2Fdjango-sentry-1.13.5.tar.gz
  Running setup.py egg_info for...

Download full text (51.6 KiB)

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/bin/python
Installing distribute..................................................................................................................................................................................done.
Downloading/unpacking configglue==1.0.1 (from -r requirements.txt (line 1))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fc%2Fconfigglue%2Fconfigglue-1.0.1.tar.gz
  Running setup.py egg_info for package configglue
Downloading/unpacking coverage (from -r requirements.txt (line 2))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fc%2Fcoverage%2Fcoverage-3.5.2.tar.gz
  Running setup.py egg_info for package coverage
    no previously-included directories found matching 'test'
Downloading/unpacking django==1.3 (from -r requirements.txt (line 3))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2FD%2FDjango%2FDjango-1.3.tar.gz
  Running setup.py egg_info for package django
Downloading/unpacking django-configglue==0.6.1 (from -r requirements.txt (line 4))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-configglue%2Fdjango-configglue-0.6.1.tar.gz
  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://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk (from -r requirements.txt (line 5))
  Checking out http://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk to ./virtualenv/src/django-pgtools
  Running setup.py egg_info for package django-pgtools
Obtaining django-openid-auth from bzr+http://bazaar.launchpad.net/~django-openid-auth/django-openid-auth/trunk (from -r requirements.txt (line 6))
  Checking out http://bazaar.launchpad.net/~django-openid-auth/django-openid-auth/trunk to ./virtualenv/src/django-openid-auth
  Running setup.py egg_info for package django-openid-auth
Downloading/unpacking django-piston==0.2.2.1 (from -r requirements.txt (line 7))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-piston%2Fdjango-piston-0.2.2.1.tar.gz
  Running setup.py egg_info for package django-piston
Downloading/unpacking django-preflight (from -r requirements.txt (line 8))
  Using download cache from /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-preflight%2Fdjango-preflight-0.1.3.tar.gz
  Running setup.py egg_info for package django-preflight
    warning: no files found matching 'templates/preflight/*.html'
Downloading/unpacking django-sentry (from -r requirements.txt (line 9))
  Storing download in cache at /home/ubuntu/.pip/cache/http%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2Fd%2Fdjango-sentry%2Fdjango-sentry-1.13.5.tar.gz
  Running setup.py egg_info ...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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()

Subscribers

People subscribed via source and target branches