Added shot summary per project on /attract

This commit is contained in:
2016-10-05 14:42:01 +02:00
parent bf35b0a9b7
commit d9a41c11b7
7 changed files with 165 additions and 17 deletions

View File

@@ -2,6 +2,7 @@ import logging
import attract.shots
import flask
import flask_login
from werkzeug.local import LocalProxy
from pillar.extension import PillarExtension
@@ -86,6 +87,27 @@ class AttractExtension(PillarExtension):
tasks.setup_app(app)
eve_hooks.setup_app(app)
def attract_projects(self):
"""Returns projects set up for Attract.
:returns: {'_items': [proj, proj, ...], '_meta': Eve metadata}
"""
import pillarsdk
from pillar.web.system_util import pillar_api
from .node_types.shot import node_type_shot
api = pillar_api()
# Find projects that are set up for Attract.
projects = pillarsdk.Project.all({
'where': {
'extension_props.attract': {'$exists': 1},
'node_types.name': node_type_shot['name'],
}}, api=api)
return projects
def _get_current_attract():
"""Returns the Attract extension of the current application."""

View File

@@ -19,11 +19,19 @@ def index():
user = flask_login.current_user
if user.is_authenticated:
tasks = current_attract.task_manager.tasks_for_user(user.objectid)
else:
tasks = None
projects = current_attract.attract_projects()
projs_with_summaries = [
(proj, current_attract.shot_manager.shot_status_summary(proj['_id']))
for proj in projects['_items']
]
return render_template('attract/index.html',
tasks=tasks)
tasks=tasks,
projs_with_summaries=projs_with_summaries)
def error_project_not_setup_for_attract():

View File

@@ -35,6 +35,38 @@ VALID_PATCH_OPERATIONS = {
log = logging.getLogger(__name__)
class ProjectSummary(object):
"""Summary of the shots in a project."""
def __init__(self):
self._counts = collections.defaultdict(int)
self._total = 0
def count(self, status):
self._counts[status] += 1
self._total += 1
def percentages(self):
"""Generator, yields (status, percentage) tuples.
The percentage is on a 0-100 scale.
"""
remaining = 100
last_index = len(self._counts) - 1
for idx, status in enumerate(sorted(self._counts.keys())):
if idx == last_index:
yield (status, remaining)
continue
perc = float(self._counts[status]) / self._total
whole_perc = int(round(perc * 100))
remaining -= whole_perc
yield (status, whole_perc)
@attr.s
class ShotManager(object):
_log = attrs_extra.log('%s.ShotManager' % __name__)
@@ -135,6 +167,35 @@ class ShotManager(object):
shot.patch(patch, api=api)
def shot_status_summary(self, project_id):
"""Returns number of shots per shot status for the given project.
:rtype: ProjectSummary
"""
api = pillar_api()
# TODO: turn this into an aggregation call to do the counting on MongoDB.
shots = pillarsdk.Node.all({
'where': {
'node_type': node_type_shot['name'],
'project': project_id,
},
'projection': {
'properties.status': 1,
},
'order': [
('properties.status', 1),
],
}, api=api)
# FIXME: this breaks when we hit the pagination limit.
summary = ProjectSummary()
for shot in shots['_items']:
summary.count(shot['properties']['status'])
return summary
def node_setattr(node, key, value):
"""Sets a node property by dotted key.

View File

@@ -19,15 +19,7 @@ log = logging.getLogger(__name__)
@blueprint.route('/')
def index():
api = pillar_api()
# Find projects that are set up for Attract.
projects = pillarsdk.Project.all({
'where': {
'extension_props.attract': {'$exists': 1},
'node_types.name': node_type_shot['name'],
}}, api=api)
projects = current_attract.attract_projects()
return render_template('attract/shots/index.html',
projects=projects['_items'])

View File

@@ -197,3 +197,9 @@ select.input-transparent
.btn:active
box-shadow: none
.bg-status
@include status-color-property(background-color, '', 'dark')
.fg-status
@include status-color-property(color, '', 'dark')

View File

@@ -24,13 +24,19 @@
span Activity Stuff
- }
h3 Shot statistics
h3 Stats
| {% for proj, summary in projs_with_summaries %}
h4
a(href="{{ url_for('attract.shots.perproject.index', project_url=proj.url) }}") {{ proj.name }}
.progress
.progress-bar.progress-bar-success(role="progressbar", style="width:50%")
| Final
.progress-bar.progress-bar-info(role="progressbar", style="width:30%")
| Review
.progress-bar.progress-bar-danger(role="progressbar", style="width:20%")
| ToDo
| {% for status, percentage in summary.percentages() %}
.progress-bar.bg-status(
class="status-{{status}}",
title="{{ status | undertitle }}",
role="progressbar",
style="width:{{percentage}}%")
| {{ status | undertitle }}
| {% endfor %}
| {% endfor %}
| {% endblock %}

View File

@@ -47,6 +47,7 @@ class AbstractShotTest(AbstractAttractTest):
self.assertIsInstance(shot, pillarsdk.Node)
return shot
class ShotManagerTest(AbstractShotTest):
@responses.activate
def test_tasks_for_shot(self):
@@ -124,6 +125,26 @@ class ShotManagerTest(AbstractShotTest):
self.assertEqual(u'Shoot the Pad Thai', found['description'])
self.assertNotIn(u'notes', found['properties'])
@responses.activate
def test_shot_summary(self):
shot1 = self.create_shot()
shot2 = self.create_shot()
shot3 = self.create_shot()
shot4 = self.create_shot()
with self.app.test_request_context():
# Log in as project admin user
pillar.auth.login_user(ctd.EXAMPLE_PROJECT_OWNER_ID)
self.mock_blenderid_validate_happy()
for shot, status in zip([shot1, shot2, shot3, shot4],
['todo', 'in_progress', 'todo', 'final']):
self.smngr.edit_shot(shot_id=shot['_id'],
status=status,
_etag=shot._etag)
# def shot_status_summary(self, project_id):
class NodeSetattrTest(unittest.TestCase):
def test_simple(self):
@@ -360,3 +381,35 @@ class RequiredAfterCreationTest(AbstractShotTest):
# TODO: should test editing a shot as well, but I had issues with the PillarSDK
# not handling deleting of properties.
class ProjectSummaryTest(unittest.TestCase):
def setUp(self):
from attract.shots import ProjectSummary
self.summ = ProjectSummary()
self.summ.count(u'todo')
self.summ.count(u'todo')
self.summ.count(u'in-progress')
self.summ.count(u'überhard')
self.summ.count(u'Æon Flux')
self.summ.count(u'Æon Flux')
self.summ.count(u'in-progress')
self.summ.count(u'todo')
def test_counting(self):
self.assertEqual(8, self.summ._total)
self.assertEqual(3, self.summ._counts[u'todo'])
self.assertEqual(2, self.summ._counts[u'Æon Flux'])
def test_percentages(self):
percs = list(self.summ.percentages())
self.assertEqual((u'in-progress', 25), percs[0])
self.assertEqual((u'todo', 38), percs[1])
self.assertEqual((u'Æon Flux', 25), percs[2])
# This should be rounded down, not rounded up, to ensure the sum of
# percentages is 100.
self.assertEqual((u'überhard', 12), percs[3])