Skip to content

Commit

Permalink
Team info directive (#437)
Browse files Browse the repository at this point in the history
* Show a list of team member with placeholder recorded time

* Update the tests

* Sum total time

* Show the total time on the server

* Clear whitespace

* Add a fix

* Show the name of a role instead of slug

* Send time in seconds instead of a string
  • Loading branch information
paopow committed Jan 24, 2018
1 parent 9889878 commit 270cbe5
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 15 deletions.
14 changes: 14 additions & 0 deletions orchestra/project_api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from rest_framework import serializers

from orchestra.models import Iteration
Expand Down Expand Up @@ -104,6 +105,7 @@ def get_assignments(self, obj):


class TaskAssignmentSerializer(serializers.ModelSerializer):
recorded_work_time = serializers.SerializerMethodField()

class Meta:
model = TaskAssignment
Expand All @@ -115,6 +117,7 @@ class Meta:
'status',
'in_progress_task_data',
'iterations',
'recorded_work_time'
)

worker = serializers.SerializerMethodField()
Expand Down Expand Up @@ -154,6 +157,17 @@ def get_in_progress_task_data(self, obj):
"""
return obj.in_progress_task_data

def get_recorded_work_time(self, obj):
query = TimeEntry.objects.filter(worker=obj.worker, assignment=obj)
if not query:
return None

total_time = datetime.timedelta()
for t in query:
total_time += t.time_worked

return total_time.total_seconds()


class TimeEntrySerializer(serializers.ModelSerializer):

Expand Down
41 changes: 41 additions & 0 deletions orchestra/project_api/tests/test_project_api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from unittest.mock import patch

from django.conf import settings
Expand All @@ -11,6 +12,7 @@
from orchestra.models import Project
from orchestra.models import Task
from orchestra.models import TaskAssignment
from orchestra.models import TimeEntry
from orchestra.models import WorkerCertification
from orchestra.project_api.api import MalformedDependencyException
from orchestra.project_api.api import get_workflow_steps
Expand All @@ -20,6 +22,7 @@
from orchestra.tests.helpers.fixtures import setup_models
from orchestra.tests.helpers.google_apps import mock_create_drive_service
from orchestra.utils.load_json import load_encoded_json
from orchestra.utils.task_lifecycle import get_new_task_assignment


class ProjectAPITestCase(OrchestraTestCase):
Expand Down Expand Up @@ -112,6 +115,7 @@ def delete_keys(obj):
'status': 'Requested Review',
'submitted_data': {'test_key': 'test_value'},
}],
'recorded_work_time': 30*60,
}],
'latest_data': {
'test_key': 'test_value'
Expand Down Expand Up @@ -175,6 +179,43 @@ def delete_keys(obj):
}
})

def test_project_assignment_recorded_time(self):
project = self.projects['base_test_project']
worker = self.workers[0]

task = self.tasks['review_task']
assignment = TaskAssignment.objects.filter(
worker=worker, task=task).first()
other_assignment = get_new_task_assignment(
worker, Task.Status.AWAITING_PROCESSING)

# create 3 time entries
TimeEntry.objects.create(
worker=self.workers[0],
date=datetime.datetime.now().date(),
time_worked=datetime.timedelta(hours=1),
assignment=assignment)
TimeEntry.objects.create(
worker=self.workers[0],
date=datetime.datetime.now().date(),
time_worked=datetime.timedelta(hours=1),
assignment=other_assignment)
TimeEntry.objects.create(
worker=self.workers[0],
date=datetime.datetime.now().date(),
time_worked=datetime.timedelta(minutes=15),
assignment=assignment)

response = self.api_client.post(
'/orchestra/api/project/project_information/',
{'project_id': project.id},
format='json')
returned = load_encoded_json(response.content)
returned_task = returned['tasks']
returned_assignment = returned_task['step1']['assignments'][0]
recorded_time = returned_assignment['recorded_work_time']
self.assertEqual(recorded_time, 105*60) # 1:15 + 0:30

def test_get_workflow_steps(self):
# See orchestra.tests.helpers.fixtures for workflow description
steps = get_workflow_steps('w3', 'crazy_workflow')
Expand Down
159 changes: 145 additions & 14 deletions orchestra/static/dist/main.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion orchestra/static/orchestra/main.es6.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import projectManagement from 'orchestra/project-management/project-management.m
import task from 'orchestra/task/task.module.es6.js'
import timing from 'orchestra/timing/timing.module.es6.js'
import todos from 'orchestra/todos/todos.module.es6.js'
import teamInfo from 'orchestra/team-info/team-info.module.es6.js'

import config from 'orchestra/config.es6.js'

Expand All @@ -21,7 +22,7 @@ angular.module('orchestra.workflows', window.orchestra.angular_modules)

angular
.module('orchestra', [
'ngRoute', common, timing, dashboard, task, todos,
'ngRoute', common, timing, dashboard, task, todos, teamInfo,
projectManagement, 'orchestra.analytics', 'orchestra.workflows'
])
.config(config)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { reduce } from 'lodash'
import template from './team-info-card.html'
import moment from 'moment-timezone'

export default function teamInfoCard (orchestraApi) {
'ngAnnotate'
return {
template,
restrict: 'E',
scope: {
projectId: '='
},
controllerAs: 'teamInfoCard',
bindToController: true,
controller: ($scope) => {
const teamInfoCard = $scope.teamInfoCard
orchestraApi.projectInformation(teamInfoCard.projectId)
.then(response => {
const {steps, tasks} = response.data
const humanSteps = new Set(steps.filter(step => step.is_human).map(step => step.slug))
teamInfoCard.steps = reduce(
Object.values(response.data.steps), (result, step) => {
result[step.slug] = step
return result
}, {})
teamInfoCard.assignments = []
for (let stepSlug of humanSteps.values()) {
const task = tasks[stepSlug]
if (task) {
teamInfoCard.assignments = teamInfoCard.assignments.concat(task.assignments.map(a => {
return {
role: teamInfoCard.steps[stepSlug].name,
worker: a.worker,
recordedTime: moment.duration(a.recorded_work_time, 'seconds').roundMinute().humanizeUnits()
}
}))
}
}
})
}
}
}
31 changes: 31 additions & 0 deletions orchestra/static/orchestra/team-info/team-info-card.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<section class="section-panel todo-list">
<div class="container-fluid">
<div class="row section-header">
<div class="col-lg-12 col-md-12 col-sm-12">
<h3>
Team info
</h3>
</div>
</div>
<div class="row section-body">
<div class="col-lg-12 col-md-12 col-sm-12">
<table class="table table-striped">
<thead>
<th>Role</th>
<th>Username</th>
<th>Name</th>
<th>Recorded time spent</th>
</thead>
<tbody>
<tr ng-repeat="assignment in teamInfoCard.assignments">
<td>{{assignment.role}}</td>
<td>{{assignment.worker.username}}</td>
<td>{{assignment.worker.first_name}} {{assignment.worker.last_name}}</td>
<td>{{assignment.recordedTime}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
10 changes: 10 additions & 0 deletions orchestra/static/orchestra/team-info/team-info.module.es6.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* global angular */

import common from 'orchestra/common/common.module.es6.js'
import teamInfoCard from 'orchestra/team-info/team-info-card.directive.es6.js'

const name = 'orchestra.teamInfo'
angular.module(name, ['ui.select', 'ngSanitize', common])
.directive('teamInfoCard', teamInfoCard)

export default name

0 comments on commit 270cbe5

Please sign in to comment.