Commit 06f37a7f by Kálmán Viktor

Merge branch 'feature-activity-detail' into 'master'

Feature: InstanceActivity details view

* add view
* tests
* closes #140
parents 9e2f21e1 8e08e6c2
......@@ -79,6 +79,14 @@ html {
color: #fff;
}
.timeline .activity-active .timeline-icon {
background-color: black!important;
}
.timeline a {
color: black;
}
.timeline-icon.timeline-warning {
border-color: #c09853;
border-style: solid;
......@@ -100,6 +108,10 @@ html {
border-left: 3px solid green;
}
.sub-activity-active {
border-left: 8px solid black;
}
.sub-activity-failed {
border-left: 3px solid #d9534f;
}
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="page-header">
<h1>
{{ object.instance.name }}: {{ object.get_readable_name }}
</h1>
</div>
<div class="row">
<div class="col-md-4" id="vm-info-pane">
<div class="big">
<span id="vm-activity-state" class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}error{% endif %}{% endif %}">
<span>{{ object.get_status_id|upper }}</span>
</span>
</div>
<div id="vm-activity-context" class="timeline">
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
</div>
<div class="col-md-8">
<div class="panel panel-default">
<!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> -->
<div class="panel-body">
<dl>
<dt>{% trans "activity code" %}</dt>
<dd>{{object.activity_code}}</dd>
<dt>{% trans "instance" %}</dt>
<dd><a href="{{object.instance.get_absolute_url}}">{{object.instance}}</a></dd>
<dt>{% trans "time" %}</dt>
<dd>{{object.started|default:'n/a'}} → {{object.finished|default:'n/a'}}</dd>
<dt>{% trans "user" %}</dt>
<dd>{{object.user|default:'(system)'}}</dd>
<dt>{% trans "type" %}</dt>
<dd>
{% if object.parent %}
{% blocktrans with url=object.parent.get_absolute_url name=object.parent %}
subactivity of <a href="{{url}}">{{name}}</a>
{% endblocktrans %}
{% else %}{% trans "top level activity" %}{% endif %}
</dd>
<dt>{% trans "task uuid" %}</dt>
<dd>{{ object.task_uuid|default:'n/a' }}</dd>
<dt>{% trans "status" %}</dt>
<dd>{{ object.get_status_id }}</dd>
<dt>{% trans "result" %}</dt>
<dd><textarea class="form-control">{{object.result}}</textarea></dd>
<dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% load i18n %}
{% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}">
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i>
</span>
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{{ a.get_readable_name }}
{% if user.is_superuser %}<a href="{{ a.get_absolute_url }}">{% endif %}
{{ a.get_readable_name }}{% if user.is_superuser %}</a>{% endif %}
</strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %}
<div class="sub-timeline">
{% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}">
<span{% if user.is_superuser and s.result %} title="{{ s.result }}"{% endif %}>
{{ s.get_readable_name }}</span> &ndash;
{% if user.is_superuser %}<a href="{{ s.get_absolute_url }}">{% endif %}
{{ s.get_readable_name }}{% if user.is_superuser %}</a>{% endif %}</span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......
import unittest
from factory import Factory, Sequence
from mock import patch, MagicMock
from django.contrib.auth.models import User
# from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, Http404
from dashboard.views import InstanceActivityDetail, InstanceActivity
class ViewUserTestCase(unittest.TestCase):
def test_404(self):
view = InstanceActivityDetail.as_view()
request = FakeRequestFactory(superuser=True)
with self.assertRaises(Http404):
view(request, pk=1234)
def test_not_superuser(self):
request = FakeRequestFactory(superuser=False)
with patch.object(InstanceActivityDetail, 'get_object') as go:
go.return_value = MagicMock(spec=InstanceActivity)
go.return_value._meta.object_name = "InstanceActivity"
view = InstanceActivityDetail.as_view()
self.assertEquals(view(request, pk=1234).status_code, 302)
def test_found(self):
request = FakeRequestFactory(superuser=True)
with patch.object(InstanceActivityDetail, 'get_object') as go:
act = MagicMock(spec=InstanceActivity)
act._meta.object_name = "InstanceActivity"
go.return_value = act
view = InstanceActivityDetail.as_view()
self.assertEquals(view(request, pk=1234).render().status_code, 200)
def FakeRequestFactory(*args, **kwargs):
''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for
mocking out django views; they are MUCH faster than the Django test client.
'''
user = UserFactory()
user.is_authenticated = lambda: kwargs.get('authenticated', True)
user.is_superuser = kwargs.get('superuser', False)
request = HttpRequest()
request.user = user
request.session = kwargs.get('session', {})
if kwargs.get('POST'):
request.method = 'POST'
request.POST = kwargs.get('POST')
else:
request.method = 'GET'
request.POST = kwargs.get('GET', {})
return request
class UserFactory(Factory):
''' using the excellent factory_boy library '''
FACTORY_FOR = User
username = Sequence(lambda i: 'test%d' % i)
first_name = 'John'
last_name = 'Doe'
email = Sequence(lambda i: 'test%d@example.com' % i)
......@@ -3,15 +3,15 @@ from django.conf.urls import patterns, url
from vm.models import Instance
from .views import (
AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete,
GroupDetailView, GroupList, GroupUserDelete, IndexView, LeaseCreate,
LeaseDelete, LeaseDetail, MyPreferencesView, NodeAddTraitView, NodeCreate,
NodeDelete, NodeDetailView, NodeFlushView, NodeGraphView, NodeList,
NodeStatus, NotificationView, PortDelete, TemplateAclUpdateView,
TemplateCreate, TemplateDelete, TemplateDetail, TemplateList,
TransferOwnershipConfirmView, TransferOwnershipView, vm_activity, VmCreate,
VmDelete, VmDetailView, VmDetailVncTokenView, VmGraphView, VmList,
VmMassDelete, VmMigrateView, VmRenewView, DiskRemoveView,
get_disk_download_status,
GroupDetailView, GroupList, GroupUserDelete, IndexView,
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView, DiskRemoveView, get_disk_download_status,
)
urlpatterns = patterns(
......@@ -57,6 +57,8 @@ urlpatterns = patterns(
name='dashboard.views.vm-migrate'),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'),
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
......
......@@ -2213,3 +2213,16 @@ def get_disk_download_status(request, pk):
}),
content_type="application/json",
)
class InstanceActivityDetail(SuperuserRequiredMixin, DetailView):
model = InstanceActivity
template_name = 'dashboard/instanceactivity_detail.html'
def get_context_data(self, **kwargs):
ctx = super(InstanceActivityDetail, self).get_context_data(**kwargs)
ctx['activities'] = (
self.object.instance.activity_log.filter(parent=None).
order_by('-started').select_related('user').
prefetch_related('children'))
return ctx
......@@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals
from contextlib import contextmanager
from logging import getLogger
from django.core.urlresolvers import reverse
from django.db.models import CharField, ForeignKey
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
......@@ -42,9 +43,20 @@ class InstanceActivity(ActivityModel):
return '{}({})'.format(self.activity_code,
self.instance)
def get_absolute_url(self):
return reverse('dashboard.views.vm-activity', args=[self.pk])
def get_readable_name(self):
return self.activity_code.split('.')[-1].replace('_', ' ').capitalize()
def get_status_id(self):
if self.succeeded is None:
return 'wait'
elif self.succeeded:
return 'success'
else:
return 'failed'
@classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None,
concurrency_check=True):
......
......@@ -4,3 +4,4 @@ coverage==3.6
django-discover-runner==0.4
django-nose==1.2
mock==1.0.1
factory-boy==2.3.1
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment