зеркало из https://github.com/mozilla/kitsune.git
[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:
Родитель
25aff61ec2
Коммит
afaac7af31
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче