Skip to content

Commit

Permalink
[bug 718698] KB Article votes in KPI dashboard.
Browse files Browse the repository at this point in the history
* Refactorings to api views for reuse.
* Refactored js into a backbone app.
  • Loading branch information
rlr committed Jan 23, 2012
1 parent c937118 commit 116851b
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 124 deletions.
139 changes: 104 additions & 35 deletions apps/kpi/api.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from operator import itemgetter, attrgetter
from datetime import date
from operator import itemgetter
from datetime import date, timedelta

from django.db.models import Count

from tastypie.resources import Resource
from tastypie import fields
from tastypie.authentication import BasicAuthentication
from tastypie.authorization import Authorization

from questions.models import Question
from wiki.models import HelpfulVote


class PermissionAuthorization(Authorization):
Expand All @@ -24,56 +24,38 @@ class Struct:
def __init__(self, **entries):
self.__dict__.update(entries)

def __unicode__(self):
return unicode(self.__dict__)


class SolutionResource(Resource):
"""
Returns the number of questions with
and without an answer maked as the solution.
"""
date = fields.DateField('date')
with_solutions = fields.IntegerField('solutions', default=0)
without_solutions = fields.IntegerField('without_solutions', default=0)
solved = fields.IntegerField('solved', default=0)
questions = fields.IntegerField('questions', default=0)

def get_object_list(self, request):
# TODO: Cache the result.

# Set up the query for the data we need
qs = Question.objects.extra(
qs = Question.objects.filter(created__gte=_start_date()).extra(
select={
'month': 'extract( month from created )',
'year': 'extract( year from created )',
}).values('year', 'month').annotate(count=Count('created'))

# Filter on solution
qs_without_solutions = qs.exclude(solution__isnull=False)
qs_with_solutions = qs.filter(solution__isnull=False)

# Wonky re mapping to go from
# [{'date': '8-2011', 'solutions': 707},
# {'date': '7-2011', 'solutions': 740},...]
# to
# {'1-2010': {'solutions': 1},
# '1-2011': {'solutions': 294},...}
w = dict((date(x['year'], x['month'], 1),
{'solutions': x['count']}) for x in qs_with_solutions)
wo = dict((date(x['year'], x['month'], 1),
{'without_solutions': x['count']})
for x in qs_without_solutions)

# merge the reuslts
# [{ "date": "2011-10-01",
# "resource_uri": "",
# "with_solutions": 1,
# "without_solutions": 5
# }, {
# "date": "2011-08-01",
# "resource_uri": "",
# "with_solutions": 707,
# "without_solutions": 8144
# }, ...]
res_dict = dict((s, dict(w.get(s, {}).items() + wo.get(s, {}).items()))
for s in set(w.keys() + wo.keys()))
res_list = [dict(date=k, **v) for k, v in res_dict.items()]
return [Struct(**x) for x in sorted(res_list, key=itemgetter('date'),
reverse=True)]
# Remap
w = _remap_date_counts(qs_with_solutions, 'solved')
wo = _remap_date_counts(qs, 'questions')

# Merge
return _merge_list_of_dicts('date', w, wo)

def obj_get_list(self, request=None, **kwargs):
return self.get_object_list(request)
Expand All @@ -82,3 +64,90 @@ class Meta:
resource_name = 'kpi_solution'
allowed_methods = ['get']
authorization = PermissionAuthorization('users.view_kpi_dashboard')


class ArticleVoteResource(Resource):
"""
Returns the number of total and helpful votes.
"""
date = fields.DateField('date')
helpful = fields.IntegerField('helpful', default=0)
votes = fields.IntegerField('votes', default=0)

def get_object_list(self, request):
# TODO: Cache the result.

# Set up the query for the data we need
qs = HelpfulVote.objects.filter(created__gte=_start_date()).extra(
select={
'month': 'extract( month from created )',
'year': 'extract( year from created )',
}).values('year', 'month').annotate(count=Count('created'))

# Filter on helpful
qs_helpful_votes = qs.filter(helpful=True)

# Remap
votes = _remap_date_counts(qs, 'votes')
helpful = _remap_date_counts(qs_helpful_votes, 'helpful')

# Merge
return _merge_list_of_dicts('date', votes, helpful)

def obj_get_list(self, request=None, **kwargs):
return self.get_object_list(request)

class Meta:
resource_name = 'kpi_kbvote'
allowed_methods = ['get']
authorization = PermissionAuthorization('users.view_kpi_dashboard')


def _start_date():
"""The date from which we start querying data."""
# Lets start on the first day of the month a year ago
year_ago = date.today() - timedelta(days=365)
return date(year_ago.year, year_ago.month, 1)


def _remap_date_counts(qs, label):
"""Remap the query result.
From: [{'count': 2085, 'month': 11, 'year': 2010},...]
To: {'<label>': 2085, 'date': '2010-11-01'}
"""
return [{'date': date(x['year'], x['month'], 1), label: x['count']}
for x in qs]


def _merge_list_of_dicts(key, *args):
"""Merge a lists of dicts into one list, grouping them by key.
All dicts in the lists must have the specified key.
From:
[{"date": "2011-10-01", "votes": 3},...]
[{"date": "2011-10-01", "helpful": 7},...]
...
To:
[{"date": "2011-10-01", "votes": 3, "helpful": 7, ...},...]
"""
result_dict = {}
result_list = []

# Build the dict
for l in args:
for d in l:
val = d.pop(key)
if val in result_dict:
result_dict[val].update(d)
else:
result_dict[val] = d

# Convert to a list
for k in sorted(result_dict.keys(), reverse=True):
d = result_dict[k]
d.update({key: k})
result_list.append(Struct(**d))

return result_list
12 changes: 7 additions & 5 deletions apps/kpi/templates/kpi/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
{% set title = _('KPI Dashboard') %}

{% block content %}
<article id="kpi-dash"
data-percent-answered-url="{{ url('api_dispatch_list', resource_name='kpi_solution', api_name ='v1') }}">
<h1>{{ title }}</h1>
<div id="percent_answered"></div>
</article>
<article id="kpi-dash">

<h1>{{ title }}</h1>
<div id="kpi-dash-app"
data-solved-url="{{ url('api_dispatch_list', resource_name='kpi_solution', api_name ='v1') }}"
data-vote-url="{{ url('api_dispatch_list', resource_name='kpi_kbvote', api_name ='v1') }}">
</div>

</article>
{% endblock %}
74 changes: 30 additions & 44 deletions apps/kpi/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,55 @@
import json

import mock
from nose.tools import eq_
import waffle

from questions.models import Question, Answer, User
from users.models import Profile

from sumo.helpers import urlparams
from questions.tests import question, answer
from sumo.tests import TestCase, LocalizingClient
from sumo.urlresolvers import reverse
from users.tests import user, add_permission
from users.models import Profile
from wiki.tests import revision, helpful_vote


class KpiAPITests(TestCase):
client_class = LocalizingClient

@mock.patch.object(waffle, 'switch_is_active')
def test_percent(self, switch_is_active):
"""Test user API with all defaults."""
switch_is_active.return_value = True
u = user()
u.save()
add_permission(u, models.Profile , 'view_dashboard')
import json

import mock
from nose.tools import eq_
import waffle
def test_solved(self):
"""Test solved API call."""
u = user(save=True)
add_permission(u, Profile, 'view_kpi_dashboard')

from questions.models import Question, Answer, User
a = answer(save=True)
a.question.solution = a
a.question.save()

from sumo.helpers import urlparams
from sumo.tests import TestCase, LocalizingClient
from sumo.urlresolvers import reverse
from users.tests import user, add_permission
from users.models import Profile
question(save=True)

class KpiAPITests(TestCase):
client_class = LocalizingClient
url = reverse('api_dispatch_list',
kwargs={'resource_name': 'kpi_solution',
'api_name': 'v1'})
self.client.login(username=u.username, password='testpass')
response = self.client.get(url + '?format=json')
eq_(200, response.status_code)
r = json.loads(response.content)
eq_(r['objects'][0]['solved'], 1)
eq_(r['objects'][0]['questions'], 2)

@mock.patch.object(waffle, 'switch_is_active')
def test_percent(self, switch_is_active):
"""Test user API with all defaults."""
switch_is_active.return_value = True
u = user()
u.save()
def test_vote(self):
"""Test vote API call."""
u = user(save=True)
add_permission(u, Profile, 'view_kpi_dashboard')
question = Question(title='Test Question',
content='Lorem Ipsum Dolor',
creator_id=u.id)
question.save()
answer = Answer(question=question, creator_id=u.id,
content="Test Answer")
answer.save()

question.solution = answer
question.save()
r = revision(save=True)
helpful_vote(revision=r, save=True)
helpful_vote(revision=r, save=True)
helpful_vote(revision=r, helpful=True, save=True)

url = reverse('api_dispatch_list',
kwargs={'resource_name': 'kpi_solution',
kwargs={'resource_name': 'kpi_kbvote',
'api_name': 'v1'})
self.client.login(username=u.username, password='testpass')
response = self.client.get(url + '?format=json')
eq_(200, response.status_code)
r = json.loads(response.content)
eq_(r['objects'][0]['with_solutions'], 1)
eq_(r['objects'][0]['without_solutions'], 0)
eq_(r['objects'][0]['helpful'], 1)
eq_(r['objects'][0]['votes'], 3)
3 changes: 2 additions & 1 deletion apps/kpi/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django.conf.urls.defaults import patterns, url, include
from tastypie.api import Api
from kpi.api import SolutionResource
from kpi.api import SolutionResource, ArticleVoteResource

v1_api = Api(api_name='v1')
v1_api.register(SolutionResource())
v1_api.register(ArticleVoteResource())


urlpatterns = patterns('kpi.views',
Expand Down
10 changes: 9 additions & 1 deletion apps/wiki/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

from sumo.tests import LocalizingClient, TestCase, with_save
from users.tests import get_user
from wiki.models import Document, Revision, CATEGORIES, SIGNIFICANCES
from wiki.models import (Document, Revision, HelpfulVote,
CATEGORIES, SIGNIFICANCES)


class TestCaseBase(TestCase):
Expand Down Expand Up @@ -51,6 +52,13 @@ def revision(**kwargs):
return Revision(**defaults)


@with_save
def helpful_vote(**kwargs):
defaults = dict(created=datetime.now(), helpful=False)
defaults.update(kwargs)
return HelpfulVote(**defaults)


def translated_revision(locale='de', save=False, **kwargs):
"""Return a revision that is the translation of a default-language one."""
parent_rev = revision(is_approved=True,
Expand Down
15 changes: 15 additions & 0 deletions media/css/kpi.dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,18 @@
#main {
background: none;
}

#kpi-dash {
min-height: 600px;
}

#kpi-dash section {
background: url('../img/wait-trans.gif') no-repeat center center;
height: 400px;
margin: 20px 0;
width: 800px;
}

#kpi-dash section.loaded {
background: none;
}
Loading

0 comments on commit 116851b

Please sign in to comment.