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 { ...@@ -79,6 +79,14 @@ html {
color: #fff; color: #fff;
} }
.timeline .activity-active .timeline-icon {
background-color: black!important;
}
.timeline a {
color: black;
}
.timeline-icon.timeline-warning { .timeline-icon.timeline-warning {
border-color: #c09853; border-color: #c09853;
border-style: solid; border-style: solid;
...@@ -100,6 +108,10 @@ html { ...@@ -100,6 +108,10 @@ html {
border-left: 3px solid green; border-left: 3px solid green;
} }
.sub-activity-active {
border-left: 8px solid black;
}
.sub-activity-failed { .sub-activity-failed {
border-left: 3px solid #d9534f; 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 %} {% load i18n %}
{% for a in activities %} {% 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 %}"> <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> <i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i>
</span> </span>
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}> <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> </strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %} {{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %} {% if a.children.count > 0 %}
<div class="sub-timeline"> <div class="sub-timeline">
{% for s in a.children.all %} {% 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 %}> <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 %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
{% else %} {% 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 ...@@ -3,15 +3,15 @@ from django.conf.urls import patterns, url
from vm.models import Instance from vm.models import Instance
from .views import ( from .views import (
AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete, AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete,
GroupDetailView, GroupList, GroupUserDelete, IndexView, LeaseCreate, GroupDetailView, GroupList, GroupUserDelete, IndexView,
LeaseDelete, LeaseDetail, MyPreferencesView, NodeAddTraitView, NodeCreate, InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
NodeDelete, NodeDetailView, NodeFlushView, NodeGraphView, NodeList, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeStatus, NotificationView, PortDelete, TemplateAclUpdateView, NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus,
TemplateCreate, TemplateDelete, TemplateDetail, TemplateList, NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TransferOwnershipConfirmView, TransferOwnershipView, vm_activity, VmCreate, TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
VmDelete, VmDetailView, VmDetailVncTokenView, VmGraphView, VmList, TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmMassDelete, VmMigrateView, VmRenewView, DiskRemoveView, VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
get_disk_download_status, VmRenewView, DiskRemoveView, get_disk_download_status,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -57,6 +57,8 @@ urlpatterns = patterns( ...@@ -57,6 +57,8 @@ urlpatterns = patterns(
name='dashboard.views.vm-migrate'), name='dashboard.views.vm-migrate'),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(), url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'), 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/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(), url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
......
...@@ -2213,3 +2213,16 @@ def get_disk_download_status(request, pk): ...@@ -2213,3 +2213,16 @@ def get_disk_download_status(request, pk):
}), }),
content_type="application/json", 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 ...@@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals
from contextlib import contextmanager from contextlib import contextmanager
from logging import getLogger from logging import getLogger
from django.core.urlresolvers import reverse
from django.db.models import CharField, ForeignKey from django.db.models import CharField, ForeignKey
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -42,9 +43,20 @@ class InstanceActivity(ActivityModel): ...@@ -42,9 +43,20 @@ class InstanceActivity(ActivityModel):
return '{}({})'.format(self.activity_code, return '{}({})'.format(self.activity_code,
self.instance) self.instance)
def get_absolute_url(self):
return reverse('dashboard.views.vm-activity', args=[self.pk])
def get_readable_name(self): def get_readable_name(self):
return self.activity_code.split('.')[-1].replace('_', ' ').capitalize() 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 @classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None, def create(cls, code_suffix, instance, task_uuid=None, user=None,
concurrency_check=True): concurrency_check=True):
......
...@@ -4,3 +4,4 @@ coverage==3.6 ...@@ -4,3 +4,4 @@ coverage==3.6
django-discover-runner==0.4 django-discover-runner==0.4
django-nose==1.2 django-nose==1.2
mock==1.0.1 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