[581108, 581584] Implement auto-tagging based on product, category, OS, and FF version. Drop "Intel" and "PPC" from Mac OS names.

* Tags are added for full and tenths-place variants (e.g., 4.0.1 and 4.0) of known Firefox versions.
* An OS will be auto-tagged if there exists a tag with its name (case insensitive). (I posit ease of administration outweighs risk of abuse.)
* Finished off questions_config based on Cww's etherpad.
* Show user agent string in System Details pop-up.
This commit is contained in:
Erik Rose 2010-07-28 17:35:27 -07:00
Родитель 25aff61ec2
Коммит afaac7af31
11 изменённых файлов: 146 добавлений и 37 удалений

Просмотреть файл

@ -1,4 +1,5 @@
from datetime import datetime, timedelta
import re
from django.db import models
from django.db.models.signals import post_save
@ -6,6 +7,8 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes import generic
import jinja2
import product_details
from taggit.models import Tag
from notifications.tasks import delete_watches
from sumo.models import ModelBase, TaggableMixin
@ -13,9 +16,9 @@ from sumo.parser import WikiParser
from sumo.urlresolvers import reverse
from sumo.helpers import urlparams
import questions as constants
from questions.tags import add_existing_tag
from .question_config import products
from .tasks import update_question_votes, build_answer_notification
from notifications.tasks import delete_watches
from upload.models import ImageAttachment
@ -55,11 +58,10 @@ class Question(ModelBase, TaggableMixin):
parser = WikiParser()
return jinja2.Markup(parser.parse(self.content, False))
def save(self, *args, **kwargs):
def save(self, no_update=False, *args, **kwargs):
"""Override save method to take care of updated."""
if self.id and not kwargs.get('no_update'):
if self.id and not no_update:
self.updated = datetime.now()
kwargs.pop('no_update', None)
super(Question, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
@ -69,9 +71,11 @@ class Question(ModelBase, TaggableMixin):
def add_metadata(self, **kwargs):
"""Add (save to db) the passed in metadata.
Usage:
question = Question.objects.get(pk=1)
question.add_metadata(ff_version='3.6.3', os='Linux')
"""
for key, value in kwargs.items():
QuestionMetaData.objects.create(question=self, name=key,
@ -102,17 +106,44 @@ class Question(ModelBase, TaggableMixin):
@property
def product(self):
"""Return the product this question is about or None if unknown."""
"""Return the product this question is about or an empty mapping if
unknown."""
md = self.metadata
if 'product' in md:
return products.get(md['product'])
return products.get(md['product'], {})
return {}
@property
def category(self):
"""Return the category this question refers to or None if unknown."""
"""Return the category this question refers to or an empty mapping if
unknown."""
md = self.metadata
if self.product and 'category' in md:
return self.product['categories'].get(md['category'])
return self.product['categories'].get(md['category'], {})
return {}
def auto_tag(self):
"""Apply tags to myself that are implied by my contents."""
to_add = self.product.get('tags', []) + self.category.get('tags', [])
version = self.metadata.get('ff_version', '')
if version in product_details.firefox_history_development_releases or \
version in product_details.firefox_history_stability_releases or \
version in product_details.firefox_history_major_releases:
to_add.append('Firefox %s' % version)
tenths = _tenths_version(version)
if tenths:
to_add.append('Firefox %s' % tenths)
self.tags.add(*to_add)
# Add a tag for the OS if it already exists as a tag:
os = self.metadata.get('os')
if os:
try:
add_existing_tag(os, self.tags)
except Tag.DoesNotExist:
pass
def get_absolute_url(self):
return reverse('questions.answers',
@ -320,3 +351,18 @@ def send_vote_update_task(**kwargs):
update_question_votes.delay(q)
post_save.connect(send_vote_update_task, sender=QuestionVote)
_tenths_version_pattern = re.compile(r'(\d+\.\d+).*')
def _tenths_version(full_version):
"""Return the major and minor version numbers from a full version string.
Don't return bugfix version, beta status, or anything futher. If there is
no major or minor version in the string, return ''.
"""
match = _tenths_version_pattern.match(full_version)
if match:
return match.group(1)
return ''

Просмотреть файл

@ -6,6 +6,7 @@ products = SortedDict([
('desktop', {
'name': _('Firefox 3.6 or earlier on Desktops/Laptops/Netbooks'),
'extra_fields': ['troubleshooting', 'ff_version', 'os', 'plugins'],
'tags': ['desktop'],
'categories': SortedDict([
('d1', {
'name': _('Firefox is having problems with certain web sites'),
@ -16,12 +17,12 @@ products = SortedDict([
{'title': 'Problems using Facebook in Firefox',
'url': '/en-US/kb/Problems+using+Facebook+in+Firefox'},
],
'tag': ('websites',),
'tags': ['websites'],
}),
('d2', {
'name': _('Firefox is crashing or closing unexpectedly'),
'extra_fields': ['crash_id'],
'tag': ('crash',),
'tags': ['crash'],
}),
('d3', {
'name': _('I have a problem with my bookmarks, cookies, history or settings'),
@ -31,7 +32,7 @@ products = SortedDict([
{'title': 'Enabling and disabling cookies',
'url': '/en-US/kb/Enabling+and+disabling+cookies'},
],
'tag': ('data',),
'tags': ['data'],
}),
('d4', {
'name': _('I need help learning to use a Firefox feature'),
@ -47,7 +48,7 @@ products = SortedDict([
{'title': 'Location bar autocomplete',
'url': '/en-US/kb/Location+bar+autocomplete'},
],
'tag': ('learning',),
'tags': ['learning'],
}),
('d5', {
'name': _('I have a problem with an extension or plugin'),
@ -58,7 +59,7 @@ products = SortedDict([
'removing an extension or plugin, see <a '
'href="/en-US/kb/Uninstalling+add-ons">Uninstalling '
'add-ons</a>.',
'tag': ('addon',),
'tags': ['addon'],
}),
('d6', {
'name': _('I have another kind of problem with Firefox'),
@ -73,37 +74,38 @@ products = SortedDict([
{'title': 'Menu bar is mising',
'url': '/en-US/kb/Menu+bar+is+missing'},
],
'tag': ('general',),
'tags': ['general'],
}),
])
}),
('beta', {
'name': _('Firefox 4 betas on Desktops/Laptops/Netbooks'),
'extra_fields': ['troubleshooting', 'ff_version', 'os', 'plugins'],
'tags': ['beta'],
'categories': SortedDict([
('b1', {
'name': _("I'm having trouble with the look and feel of the Firefox beta"),
'tag': ('ui',),
'tags': ['ui'],
}),
('b2', {
'name': _('Firefox is having problems with certain web sites'),
'extra_fields': ['sites_affected'],
'tag': ('websites',),
'tags': ['websites'],
}),
('b3', {
'name': _('Firefox is crashing or closing unexpectedly'),
'extra_fields': ['crash_id'],
'tag': ('crash',),
'tags': ['crash'],
}),
('b4', {
'name': _('I have a problem with an extension or plugin'),
'extra_fields': ['addon'],
'tag': ('addon',),
'tags': ['addon'],
}),
('b5', {
'name': _('I have another kind of problem with Firefox'),
'extra_fields': ['frequency', 'started'],
'tag': ('general',),
'tags': ['general'],
}),
('b6', {
'name': _('I have feedback/suggestions about the beta'),
@ -117,11 +119,12 @@ products = SortedDict([
('mobile', {
'name': _('Firefox on Mobile (Android or Maemo systems)'),
'extra_fields': ['ff_version', 'os', 'plugins'],
'tags': ['mobile'],
'categories': SortedDict([
('m1', {
'name': _('Firefox for mobile is having problems with certain web sites'),
'extra_fields': ['sites_affected'],
'tag': ('websites',),
'tags': ['websites'],
}),
('m2', {
'name': _("I'm having trouble with the look and feel of Firefox for mobile"),
@ -135,11 +138,11 @@ products = SortedDict([
{'title': 'How to use the Location Bar ',
'url': '/en-US/kb/How+to+use+the+Location+Bar'},
],
'tag': ('ui',),
'tags': ['ui'],
}),
('m3', {
'name': _('Firefox for mobile is crashing or closing unexpectedly'),
'tag': ('crash',),
'tags': ['crash'],
}),
('m4', {
'name': _('I have a problem with an extension or plugin'),
@ -151,7 +154,7 @@ products = SortedDict([
'href="http://mobile.support.mozilla.com'
'/en-US/kb/How+to+remove+or+disable+add-ons">'
'How to remove or disable add-ons</a>.',
'tag': ('addon',),
'tags': ['addon'],
}),
('m5', {
'name': _('I have feedback/suggestions about Firefox for Mobile'),
@ -162,6 +165,7 @@ products = SortedDict([
}),
('home', {
'name': _('Firefox Home App for iPhone'),
'tags': ['FxHome'],
'categories': SortedDict([
('i1', {
'name': _("I'm having trouble setting up Firefox Home on my iPhone"),
@ -169,7 +173,7 @@ products = SortedDict([
{'title': 'How to set up Firefox Home on your iPhone',
'url': '/en-US/kb/How+to+set+up+Firefox+Home+on+your+iPhone'},
],
'tag': ('iphone',),
'tags': ['iphone'],
}),
('i2', {
'name': _("I'm having trouble setting up Firefox Sync on my Desktop Firefox"),
@ -177,7 +181,7 @@ products = SortedDict([
{'title': 'How to sync Firefox settings between computers',
'url': '/en-US/kb/How+to+sync+Firefox+settings+between+computers'},
],
'tag': ('desktop', 'sync',),
'tags': ['desktop', 'sync'],
}),
('i3', {
'name': _('Not all my data is syncing between Firefox '
@ -190,7 +194,7 @@ products = SortedDict([
{'title': 'Replace your Sync information',
'url': '/kb/Replace+your+Sync+information'},
],
'tag': ('sync',),
'tags': ['sync'],
}),
('i4', {
'name': _('I have feedback/suggestions about Firefox Home for iPhone'),
@ -201,6 +205,7 @@ products = SortedDict([
}),
('sync', {
'name': _('Firefox Sync'),
'tags': ['sync'],
'categories': SortedDict([
('s1', {
'name': _("I'm having trouble setting up Firefox Sync on my Nokia or Android device"),
@ -208,7 +213,7 @@ products = SortedDict([
{'title': 'How to sync Firefox settings with a mobile device',
'url': 'http://mobile.support.mozilla.com/en-US/kb/How+to+sync+Firefox+settings+with+a+mobile+device'},
],
'tag': ('iphone',),
'tags': ['mobile'],
}),
('s2', {
'name': _("I'm having trouble setting up Firefox Home on my iPhone"),
@ -216,7 +221,7 @@ products = SortedDict([
{'title': 'How to set up Firefox Home on your iPhone',
'url': '/en-US/kb/How+to+set+up+Firefox+Home+on+your+iPhone'},
],
'tag': ('iphone',),
'tags': ['iphone'],
}),
('s3', {
'name': _("I'm having trouble setting up Firefox Sync on my Desktop Firefox"),
@ -224,7 +229,7 @@ products = SortedDict([
{'title': 'How to sync Firefox settings between computers',
'url': '/en-US/kb/How+to+sync+Firefox+settings+between+computers'},
],
'tag': ('desktop',),
'tags': ['desktop'],
}),
('s4', {
'name': _('I have other problems syncing data between computers or devices'),
@ -238,7 +243,7 @@ products = SortedDict([
{'title': 'Replace your Sync information',
'url': '/kb/Replace+your+Sync+information'},
],
'tag': ('sync',),
'tags': ['sync'],
}),
('s5', {
'name': _('I have feedback/suggestions about Firefox Home for iPhone'),

Просмотреть файл

@ -176,6 +176,10 @@
{{ question.metadata.plugins|wiki_to_html }}
</div>
{% endif %}
{% if question.metadata.useragent %}
<h5>{{ _('User Agent') }}</h5>
<p>{{ question.metadata.useragent }}</p>
{% endif %}
</div>
</div>

Просмотреть файл

@ -3,7 +3,8 @@ from nose.tools import eq_, raises
import sumo.models
from taggit.models import Tag
from questions.models import Question, QuestionMetaData, Answer
from questions.models import (Question, QuestionMetaData, Answer,
_tenths_version)
from questions.tags import add_existing_tag
from questions.tests import TestCaseBase, TaggingTestCaseBase, tags_eq
from questions.question_config import products
@ -133,6 +134,31 @@ class TestQuestionMetadata(TestCaseBase):
"clear_mutable_metadata() didn't clear the cached metadata."
eq_(dict(product='desktop', category='d1', useragent='Fyerfocks'), md)
def test_auto_tagging(self):
"""Make sure tags get applied based on metadata on first save."""
Tag.objects.create(slug='green', name='green')
q = self.question
q.add_metadata(product='desktop', category='d1', ff_version='3.6.8',
os='GREen')
q.save()
q.auto_tag()
tags_eq(q, ['desktop', 'websites', 'Firefox 3.6.8', 'Firefox 3.6',
'green'])
def test_auto_tagging_restraint(self):
"""Auto-tagging shouldn't tag unknown Firefox versions or OSes."""
q = self.question
q.add_metadata(ff_version='allyourbase', os='toaster 1.0')
q.save()
q.auto_tag()
tags_eq(q, [])
def test_tenths_version(self):
"""Test the filter that turns 1.2.3 into 1.2."""
eq_(_tenths_version('1.2.3beta3'), '1.2')
eq_(_tenths_version('1.2rc'), '1.2')
eq_(_tenths_version('1.w'), '')
class QuestionTests(TestCaseBase):
"""Tests for Question model"""
@ -154,7 +180,7 @@ class QuestionTests(TestCaseBase):
def test_default_manager(self):
"""Assert Question's default manager is SUMO's ManagerBase.
This is easy to get wrong with taggability.
This is easy to get wrong when mixing in taggability.
"""
eq_(Question._default_manager.__class__, sumo.models.ManagerBase)

Просмотреть файл

@ -615,22 +615,22 @@ class TaggingViewTestsAsTagger(TaggingTestCaseBase):
"""Assert removing an applied tag succeeds."""
response = self.client.post(_remove_tag_url(),
data={'remove-tag-colorless': 'dummy'})
self._assertRedirectsToQuestion2(response)
self._assert_redirects_to_question_2(response)
eq_([t.name for t in Question.objects.get(pk=2).tags.all()], ['green'])
def test_remove_unapplied_tag(self):
"""Test removing an unapplied tag fails silently."""
response = self.client.post(_remove_tag_url(),
data={'remove-tag-lemon': 'dummy'})
self._assertRedirectsToQuestion2(response)
self._assert_redirects_to_question_2(response)
def test_remove_no_tag(self):
"""Make sure removing with no params provided redirects harmlessly."""
response = self.client.post(_remove_tag_url(),
data={})
self._assertRedirectsToQuestion2(response)
self._assert_redirects_to_question_2(response)
def _assertRedirectsToQuestion2(self, response):
def _assert_redirects_to_question_2(self, response):
url = 'http://testserver%s' % reverse('questions.answers',
kwargs={'question_id': 2})
self.assertRedirects(response, url)

Просмотреть файл

@ -152,6 +152,9 @@ def new_question(request):
if category:
question.add_metadata(category=category['key'])
# The first time a question is saved, automatically apply some tags:
question.auto_tag()
# Submitting the question counts as a vote
return question_vote(request, question.id)

Просмотреть файл

@ -145,6 +145,21 @@ want to use ``requirements-dev.txt`` instead ::
pip install -r requirements-dev.txt
Initializing Mozilla Product Details
------------------------------------
One of the packages kitsune uses, Mozilla Product Details, needs to fetch JSON
files containing historical Firefox version data and write them within its
package directory. To set this up...
#. Run ``./manage.py update_product_details`` to do the initial fetch.
#. Schedule the above command to run periodically. Once a day is a reasonable
choice. It will fail safely on network failure.
#. Arrange for the folder django-mozilla-product-details/product_details/json
to be writable by whomever runs ``./manage.py update_product_details`` and
readable by kitsune Python processes.
Settings
--------

Просмотреть файл

@ -885,7 +885,7 @@ ul.related li {
}
#more-system-details div.wrap {
height: 300px;
height: 350px;
overflow: auto;
}

Просмотреть файл

@ -138,6 +138,14 @@
return "Windows 7";
case "Linux i686":
return "Linux";
case "Intel Mac OS X 10.4":
case "PPC Mac OS X 10.4":
return "Mac OS X 10.4";
case "Intel Mac OS X 10.5":
case "PPC Mac OS X 10.5":
return "Mac OS X 10.5";
case "Intel Mac OS X 10.6":
return "Mac OS X 10.6";
default:
return oscpu;
}

Просмотреть файл

@ -8,6 +8,7 @@ Jinja2==2.2.1
-e git://github.com/jsocol/bleach.git#egg=bleach
-e git://github.com/jbalogh/schematic.git#egg=schematic
-e git://github.com/jsocol/django-cronjobs.git#egg=django_cronjobs
-e git://github.com/fwenzel/django-mozilla-product-details#egg=django-mozilla-product-details
GitPython==0.1.7
MySQL-python==1.2.3c1

Просмотреть файл

@ -178,6 +178,7 @@ INSTALLED_APPS = (
'taggit',
'flagit',
'upload',
'product_details'
)
# Extra apps for testing