Skip to content
This repository has been archived by the owner on Jan 31, 2018. It is now read-only.

Commit

Permalink
[bug 1129085] Infer version for Firefox Dev
Browse files Browse the repository at this point in the history
Previously, we'd only infer the version if the browser from the user
agent matched the product db_name. That doesn't work for Firefox Dev
Edition or Firefox 64.

This fixes that so that we can explicitly state which "browser" a
product is. Then it'll match up, we can appropriately infer the
product version from the user-agent specified browser version, and we
can go home at night and feel like we did a good job at work.
  • Loading branch information
willkg committed Feb 6, 2015
1 parent 973d501 commit 5b5f9b9
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 15 deletions.
1 change: 1 addition & 0 deletions fjord/analytics/analyzer_views.py
Expand Up @@ -771,6 +771,7 @@ def form_valid(self, form):
instance.enabled = form.data.get('enabled') or False
instance.on_dashboard = form.data.get('on_dashboard') or False
instance.on_picker = form.data.get('on_picker') or False
instance.browser = form.data.get('browser') or u''
instance.browser_data_browser = form.data.get('browser_data_browser') or u''
self.object = instance.save()
except Product.DoesNotExist:
Expand Down
1 change: 1 addition & 0 deletions fjord/analytics/forms.py
Expand Up @@ -44,6 +44,7 @@ class Meta:
'slug',
'on_dashboard',
'on_picker',
'browser',
'browser_data_browser',
'notes'
]
2 changes: 2 additions & 0 deletions fjord/analytics/templates/analytics/analyzer/addproducts.html
Expand Up @@ -13,6 +13,7 @@ <h2>Analytics: Products</h2>
<th>slug</th>
<th>automatic translations?</th>
<th>feedback product url</th>
<th>browser</th>
<th>browser data browser</th>
<th>notes</th>
</tr>
Expand All @@ -31,6 +32,7 @@ <h2>Analytics: Products</h2>
<td>{{ prod.translation_system }}</td>
{# FIXME - URL is hard-coded so it doesn't include the locale. Need better way to do this. #}
<td><a href="https://input.mozilla.org/feedback/{{ prod.slug }}">https://input.mozilla.org/feedback/{{ prod.slug }}</a></td>
<td>{{ prod.browser }}</td>
<td>{{ prod.browser_data_browser }}</td>
<td>{{ prod.notes }}</td>
</tr>
Expand Down
3 changes: 2 additions & 1 deletion fjord/base/browsers.py
Expand Up @@ -41,7 +41,8 @@ def parse_ua(ua):
:returns: Browser namedtuple with attributes:
- browser: "Unknown" or a browser like "Firefox", "Iceweasel", etc.
- browser: "Unknown" or a browser like "Firefox", "Iceweasel",
"Firefox for Android", etc.
- browser_version: "Unknown" or a 3 dotted section like "14.0.1",
"4.0.0", etc.
- platform: "Unknown" or a platform like "Windows", "OS X",
Expand Down
1 change: 1 addition & 0 deletions fjord/feedback/admin.py
Expand Up @@ -20,6 +20,7 @@ class ProductAdmin(admin.ModelAdmin):
'db_name',
'translation_system',
'browser_data_browser',
'browser',
'image_file',
'notes',
'slug')
Expand Down
7 changes: 7 additions & 0 deletions fjord/feedback/models.py
Expand Up @@ -92,6 +92,10 @@ class Product(ModelBase):
max_length=100, blank=True, default=u'',
help_text=u'Grab browser data for browser product')

browser = models.CharField(
max_length=30, blank=True, default=u'',
help_text=u'User agent inferred browser for this product if any')

objects = ProductManager()

@classmethod
Expand Down Expand Up @@ -369,6 +373,9 @@ def infer_product(cls, browser):
elif browser.platform in (u'', u'Unknown'):
return u''

# FIXME: We can do this because our user agent parser only
# knows about Mozilla browsers. If we ever change user agent
# parsers, we'll need to rethink this.
return u'Firefox'

@classmethod
Expand Down
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models


class Migration(SchemaMigration):

def forwards(self, orm):
# Adding field 'Product.browser'
db.add_column(u'feedback_product', 'browser',
self.gf('django.db.models.fields.CharField')(default=u'', max_length=30, blank=True),
keep_default=False)


def backwards(self, orm):
# Deleting field 'Product.browser'
db.delete_column(u'feedback_product', 'browser')


models = {
u'feedback.product': {
'Meta': {'object_name': 'Product'},
'browser': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '30', 'blank': 'True'}),
'browser_data_browser': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '100', 'blank': 'True'}),
'db_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'display_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image_file': ('django.db.models.fields.CharField', [], {'default': "u'noimage.png'", 'max_length': '100', 'null': 'True', 'blank': 'True'}),
'notes': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
'on_dashboard': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'on_picker': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'translation_system': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'})
},
u'feedback.response': {
'Meta': {'ordering': "['-created']", 'object_name': 'Response'},
'api': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'browser': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'browser_platform': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'browser_version': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'campaign': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '100', 'null': 'True', 'blank': 'True'}),
'category': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
'channel': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'country': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '4', 'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'device': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'happy': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'locale': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
'manufacturer': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'platform': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'product': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'rating': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
'source': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '100', 'null': 'True', 'blank': 'True'}),
'translated_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'url': ('fjord.base.models.EnhancedURLField', [], {'max_length': '200', 'blank': 'True'}),
'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'version': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'})
},
u'feedback.responsecontext': {
'Meta': {'object_name': 'ResponseContext'},
'data': ('fjord.base.models.JSONObjectField', [], {'default': '{}'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'opinion': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['feedback.Response']"})
},
u'feedback.responseemail': {
'Meta': {'object_name': 'ResponseEmail'},
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'opinion': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['feedback.Response']"})
},
u'feedback.responsepi': {
'Meta': {'object_name': 'ResponsePI'},
'data': ('fjord.base.models.JSONObjectField', [], {'default': '{}'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'opinion': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['feedback.Response']"})
}
}

complete_apps = ['feedback']
87 changes: 87 additions & 0 deletions fjord/feedback/south_migrations/0045_populate_browser.py
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models

class Migration(DataMigration):

def forwards(self, orm):
data = [
('Firefox', 'Firefox'),
('Firefox for Android', 'Firefox for Android'),
('Firefox OS', 'Firefox OS')
]

for db_name, browser in data:
prod = orm.Product.objects.get(db_name=db_name)
prod.browser = browser
prod.save()

def backwards(self, orm):
orm.Product.objects.update(db_name=u'')

models = {
u'feedback.product': {
'Meta': {'object_name': 'Product'},
'browser': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '30', 'blank': 'True'}),
'browser_data_browser': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '100', 'blank': 'True'}),
'db_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'display_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image_file': ('django.db.models.fields.CharField', [], {'default': "u'noimage.png'", 'max_length': '100', 'null': 'True', 'blank': 'True'}),
'notes': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
'on_dashboard': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'on_picker': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'translation_system': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'})
},
u'feedback.response': {
'Meta': {'ordering': "['-created']", 'object_name': 'Response'},
'api': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'browser': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'browser_platform': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'browser_version': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'campaign': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '100', 'null': 'True', 'blank': 'True'}),
'category': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
'channel': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'country': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '4', 'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'device': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'happy': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'locale': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
'manufacturer': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'platform': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'product': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'rating': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
'source': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '100', 'null': 'True', 'blank': 'True'}),
'translated_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'url': ('fjord.base.models.EnhancedURLField', [], {'max_length': '200', 'blank': 'True'}),
'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'version': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'})
},
u'feedback.responsecontext': {
'Meta': {'object_name': 'ResponseContext'},
'data': ('fjord.base.models.JSONObjectField', [], {'default': '{}'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'opinion': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['feedback.Response']"})
},
u'feedback.responseemail': {
'Meta': {'object_name': 'ResponseEmail'},
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'opinion': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['feedback.Response']"})
},
u'feedback.responsepi': {
'Meta': {'object_name': 'ResponsePI'},
'data': ('fjord.base.models.JSONObjectField', [], {'default': '{}'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'opinion': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['feedback.Response']"})
}
}

complete_apps = ['feedback']
symmetrical = True
29 changes: 15 additions & 14 deletions fjord/feedback/views.py
Expand Up @@ -152,10 +152,10 @@ def _handle_feedback_post(request, locale=None, product=None,
# If we have a product at this point, then it came from the
# url and it's a Product instance and we need to turn it into
# the product.db_name which is a string.
product = product.db_name
product_db_name = product.db_name
else:
# Check the POST data for the product.
product = data.get('product', '')
product_db_name = data.get('product', '')

# For the version, we try the url data, then the POST data.
version = version or data.get('version', '')
Expand All @@ -166,23 +166,24 @@ def _handle_feedback_post(request, locale=None, product=None,
# to do so.
if request.BROWSER != UNKNOWN:
# If we don't have a product, try to infer that from the user
# agent.
if not product:
product = models.Response.infer_product(request.BROWSER)
# agent information.
if not product_db_name:
product_db_name = models.Response.infer_product(request.BROWSER)

# If we have a product and it matches the user agent product,
# If we have a product and it matches the user agent browser,
# then we can infer the version and platform from the user
# agent if they're missing.
if (product and product == models.Response.infer_product(
request.BROWSER)):
if not version:
version = request.BROWSER.browser_version
if not platform:
platform = models.Response.infer_platform(
product, request.BROWSER)
if product_db_name:
product = models.Product.objects.get(db_name=product_db_name)
if product.browser and product.browser == request.BROWSER.browser:
if not version:
version = request.BROWSER.browser_version
if not platform:
platform = models.Response.infer_platform(
product_db_name, request.BROWSER)

# Make sure values are at least empty strings--no Nones.
opinion.product = product or u''
opinion.product = product_db_name or u''
opinion.version = version or u''
opinion.channel = channel or u''
opinion.platform = platform or u''
Expand Down

0 comments on commit 5b5f9b9

Please sign in to comment.