Added shot summary per project on /attract
This commit is contained in:
@@ -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."""
|
||||
|
@@ -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():
|
||||
|
@@ -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.
|
||||
|
@@ -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'])
|
||||
|
||||
|
@@ -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')
|
||||
|
@@ -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 %}
|
||||
|
@@ -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])
|
||||
|
Reference in New Issue
Block a user