diff --git a/apps/themes/cron.py b/apps/themes/cron.py index 6892ccb3..7e4ba6f8 100644 --- a/apps/themes/cron.py +++ b/apps/themes/cron.py @@ -8,8 +8,9 @@ from textcluster import Corpus, search from feedback.models import Opinion -from input import (PRODUCT_USAGE, LATEST_BETAS, OPINION_PRAISE, OPINION_ISSUE, - OPINION_IDEA, PLATFORM_USAGE) +from input import (CHANNEL_USAGE, PLATFORM_USAGE, PRODUCT_USAGE, + LATEST_BETAS, LATEST_RELEASE, OPINION_PRAISE, + OPINION_ISSUE, OPINION_IDEA) from themes.models import Theme, Item SIM_THRESHOLD = settings.CLUSTER_SIM_THRESHOLD @@ -49,39 +50,56 @@ def cluster(): base_qs = Opinion.objects.filter(locale='en-US', created__gte=week_ago) log.debug('Beginning clustering') - cluster_by_product(base_qs) + cluster_by_product_and_channel(base_qs) -def cluster_by_product(qs): - for prod in PRODUCT_USAGE: - log.debug('Clustering %s(%s)' % - (unicode(prod.pretty), LATEST_BETAS[prod])) - qs_product = qs.filter(product=prod.id, version=LATEST_BETAS[prod]) - cluster_by_feeling(qs_product, prod) +def cluster_by_product_and_channel(qs): + def get_version(channel, prod): + return LATEST_BETAS[prod] if channel == 'beta' else LATEST_RELEASE[prod] + for channel in (c.short for c in CHANNEL_USAGE): + for prod in PRODUCT_USAGE: + version = get_version(channel, prod) + log.debug('Clustering %s (%s: %s)' % + (unicode(prod.pretty), channel, version)) + qs_product = qs.filter(product=prod.id, version=version) + cluster_beta_by_feeling(qs_product, channel, prod) -def cluster_by_feeling(qs, prod): - happy = qs.filter(type=OPINION_PRAISE.id) - sad = qs.filter(type=OPINION_ISSUE.id) - ideas = qs.filter(type=OPINION_IDEA.id) - cluster_by_platform(happy, prod, 'happy') - cluster_by_platform(sad, prod, 'sad') - cluster_by_platform(ideas, prod, 'ideas') +def cluster_beta_by_feeling(qs, channel, prod): + """Cluster all products by feeling.""" + # Sentiments to be considered depend on channel. + cluster_by = { + 'beta': (OPINION_PRAISE, OPINION_ISSUE, OPINION_IDEA), + 'release': (OPINION_IDEA,), + } + for op_type in cluster_by[channel]: + type_qs = qs.filter(type=op_type.id) -def cluster_by_platform(qs, prod, feeling): - # We need to create corpii for each platform and manually inspect each + cluster_by_platform(type_qs, channel, prod, op_type.short) + + +def cluster_by_platform(qs, channel, prod, feeling): + """ + Cluster all products/feelings by platform ('all' as well as separate + platforms). + """ + dimensions = dict(product=prod.id, channel=channel, feeling=feeling) + cluster_and_save(qs, dimensions) + + # Beta only: Create corpora for each platform and inspect each # opinion and put it in the right platform bucket. + if channel == 'beta': + for platform in PLATFORM_USAGE: + dimensions['platform'] = platform.short + cluster_and_save(qs.filter(platform=platform.short), + dimensions) + +def cluster_and_save(qs, dimensions): result = cluster_queryset(qs) - dimensions = dict(product=prod.id, feeling=feeling) save_result(result, dimensions) - for platform in PLATFORM_USAGE: - result = cluster_queryset(qs.filter(platform=platform.short)) - dimensions['platform'] = platform.short - save_result(result, dimensions) - def cluster_queryset(qs): seen = {} diff --git a/apps/themes/models.py b/apps/themes/models.py index 39525989..608b2f3b 100644 --- a/apps/themes/models.py +++ b/apps/themes/models.py @@ -9,8 +9,10 @@ class Theme(ModelBase): pivot = models.ForeignKey(Opinion, related_name='group') opinions = models.ManyToManyField(Opinion, through='Item') num_opinions = models.IntegerField(default=0, db_index=True) - feeling = models.CharField(max_length=20, db_index=True) # happy or sad + feeling = models.CharField(max_length=20, db_index=True) # issue, praise, + # idea product = models.PositiveSmallIntegerField() + channel = models.CharField(max_length=20) # beta, release platform = models.CharField(max_length=255, db_index=True) created = models.DateTimeField(auto_now_add=True, db_index=True) diff --git a/apps/themes/templates/themes/index.html b/apps/themes/templates/themes/index.html index b9c95828..ae7050f3 100644 --- a/apps/themes/templates/themes/index.html +++ b/apps/themes/templates/themes/index.html @@ -12,27 +12,29 @@

{{ _('Product') }}

{{ filter_list(products) }} -
-

{{ _('Type of Feedback') }}

-
- {{ filter_list(sentiments) }} + {% if CHANNEL == 'beta' %} +
+

{{ _('Type of Feedback') }}

+
+ {{ filter_list(sentiments) }} +
-
- {# No need to show platforms if there is only one non-All platform #} - {% if platforms|length > 2 %} -
-

{{ _('Platform') }}

-
- {{ filter_list(platforms) }} + {# No need to show platforms if there is only one non-All platform #} + {% if platforms|length > 2 %} +
+

{{ _('Platform') }}

+
+ {{ filter_list(platforms) }} +
-
+ {% endif %} {% endif %}
-

{{ _('Common Themes') }}

+

{{ _('Common Themes') if CHANNEL == 'beta' else _('Frequent Ideas') }}

{{ theme_list(themes) }} {{ pager() }} diff --git a/apps/themes/views.py b/apps/themes/views.py index a003f760..64721c3a 100644 --- a/apps/themes/views.py +++ b/apps/themes/views.py @@ -7,8 +7,11 @@ import jingo from tower import ugettext as _ -from input import PRODUCTS, PLATFORMS, FIREFOX, PRODUCT_USAGE -from input.decorators import cache_page +from input import (CHANNEL_BETA, CHANNEL_RELEASE, + OPINION_PRAISE, OPINION_ISSUE, OPINION_IDEA, + PRODUCTS, PLATFORMS, FIREFOX, PRODUCT_USAGE, + get_channel) +from input.decorators import cache_page, negotiate from input.helpers import urlparams from input.urlresolvers import reverse from themes.models import Theme @@ -18,33 +21,31 @@ def _get_sentiments(request, sentiment): + """Get available sentiment filters (beta channel only).""" sentiments = [] url = request.get_full_path() f = Filter(urlparams(url, s=None), _('All'), _('All feedback'), not sentiment) - sentiments.append(f) - f = Filter(urlparams(url, s='happy'), _('Praise'), _('Praise only'), - (sentiment == 'happy')) - + f = Filter(urlparams(url, s=OPINION_PRAISE.short), _('Praise'), + _('Praise only'), (sentiment == OPINION_PRAISE.short)) sentiments.append(f) - f = Filter(urlparams(url, s='sad'), _('Issues'), _('Issues only'), - (sentiment == 'sad')) - + f = Filter(urlparams(url, s=OPINION_ISSUE.short), _('Issues'), + _('Issues only'), (sentiment == OPINION_ISSUE.short)) sentiments.append(f) - f = Filter(urlparams(url, s='ideas'), _('Ideas'), - _('Ideas only'), - (sentiment == 'ideas')) - + f = Filter(urlparams(url, s=OPINION_IDEA.short), _('Ideas'), + _('Ideas only'), (sentiment == OPINION_IDEA.short)) sentiments.append(f) + return sentiments def _get_platforms(request, product, platform): + """Get platforms (beta channel only).""" platforms = [] url = request.get_full_path() @@ -66,6 +67,7 @@ def _get_platforms(request, product, platform): def _get_products(request, product): + """Get product filters (all channels).""" products = [] url = request.get_full_path() @@ -78,10 +80,10 @@ def _get_products(request, product): @cache_page(use_get=True) -def index(request): - """List the various clusters of data we have.""" +def beta_index(request): + """List the themes clusters for beta releases.""" - qs = Theme.objects.all() + qs = Theme.objects.filter(channel=CHANNEL_BETA.short) product = request.GET.get('a', FIREFOX.short) products = _get_products(request, product) try: @@ -116,10 +118,42 @@ def index(request): return jingo.render(request, 'themes/index.html', args) +@cache_page(use_get=True) +def release_index(request): + """List the themes clusters for major releases.""" + + qs = Theme.objects.filter(channel=CHANNEL_RELEASE.short, + feeling=OPINION_IDEA.short) + product = request.GET.get('a', FIREFOX.short) + products = _get_products(request, product) + try: + qs = qs.filter(product=PRODUCTS[product].id) + except KeyError: + raise http.Http404 + + args = dict(products=products) + page = request.GET.get('page', 1) + + if qs: + pp = settings.SEARCH_PERPAGE + pager = Paginator(qs.select_related(), pp) + + try: + args['page'] = pager.page(page) + except (EmptyPage, InvalidPage): + args['page'] = pager.page(pager.num_pages) + + args['themes'] = args['page'].object_list + + return jingo.render(request, 'themes/index.html', args) + +index = negotiate(beta=beta_index, release=release_index) + + @cache_page(use_get=True) def theme(request, theme_id): try: - theme = Theme.objects.get(id=theme_id) + theme = Theme.objects.get(id=theme_id, channel=get_channel()) except Theme.DoesNotExist: raise http.Http404 diff --git a/migrations/15-themes-channel.sql b/migrations/15-themes-channel.sql new file mode 100644 index 00000000..e0fdcfba --- /dev/null +++ b/migrations/15-themes-channel.sql @@ -0,0 +1,2 @@ +ALTER TABLE `theme` ADD `channel` VARCHAR( 20 ) NOT NULL AFTER `product` ; +ALTER TABLE `theme` ADD INDEX ( `channel` ) ; diff --git a/templates/base.html b/templates/base.html index c56ae4b4..96909d1d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -51,10 +51,8 @@

{% include "includes/channel_switcher.html" %}