Add activity log functionality

The new 'acivity' app adds the possibility to
attach log items to devices. When viewing a device,
a new 'Activity' tab appears, where users can add log
items. These items can then be viewed and deleted by
other users. This way a device builds up an activity
log - this might be useful for, for example, keeping
track of how a device is doing.
This commit is contained in:
Mark Boom 2018-05-02 13:02:14 +02:00 committed by Mark Boom
parent e5454d6714
commit 9792130819
15 changed files with 282 additions and 1 deletions

View File

5
netbox/activity/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ActivityConfig(AppConfig):
name = 'activity'

15
netbox/activity/forms.py Normal file
View File

@ -0,0 +1,15 @@
from utilities.forms import BootstrapMixin, CommentField
from .models import LogItem
from django import forms
class CommentForm(BootstrapMixin, forms.ModelForm):
class Meta:
fields = [
'body',
'for_device',
'created_by',
]
model = LogItem

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-30 15:19
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('dcim', '0055_virtualchassis_ordering'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='LogItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('body', models.TextField(default='')),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('created_by', models.ForeignKey(default='1', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('for_device', models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='dcim.Device')),
],
options={
'verbose_name': 'comment',
'verbose_name_plural': 'comments',
'ordering': ['-created_at'],
},
),
]

View File

26
netbox/activity/models.py Normal file
View File

@ -0,0 +1,26 @@
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
class LogItem(models.Model):
for_device = models.ForeignKey(
'dcim.Device',
on_delete=models.CASCADE,
related_name='logs',
default='1',
)
body = models.TextField(default='')
created_at = models.DateTimeField(default=timezone.now)
created_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
default='1'
)
class Meta:
verbose_name = 'comment'
verbose_name_plural = 'comments'
ordering = ['-created_at']

9
netbox/activity/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^(?P<pk>\d+)/$', views.display_activity, name='display'),
url(r'^(\d+)/delete/(?P<pk>\d+)$', views.DeleteComment.as_view(), name='delete_comment'),
url(r'^(\d+)/add', views.AddComment.as_view(), name='add_comment')
]

61
netbox/activity/views.py Normal file
View File

@ -0,0 +1,61 @@
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.utils.safestring import mark_safe
from django.shortcuts import reverse
from dcim.models import Device
from utilities.forms import ConfirmationForm
from utilities.views import ObjectDeleteView, ObjectEditView
from . import models
from .models import LogItem
from . import forms
def display_activity(request, pk):
device = get_object_or_404(Device, pk=pk)
logItems = device.logs.all()
return render(request, 'activity/displayActivity.html', {
'device': device,
'logItems': logItems,
})
class DeleteComment(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'activity.delete_logitem'
model = LogItem
template_name = 'activity/deleteComment.html'
def get_return_url(self, request, obj):
return reverse('activity:display', kwargs={'pk': obj.for_device.pk})
class AddComment(PermissionRequiredMixin, ObjectEditView):
permission_required = 'activity.add_logitem'
model = LogItem
model_form = forms.CommentForm
template_name = 'activity/addComment.html'
def get_return_url(self, request, obj):
return reverse('activity:display', kwargs={'pk': obj.for_device.pk})
def get(self, request, *args, **kwargs):
obj = self.get_object(kwargs)
obj = self.alter_obj(obj, request, args, kwargs)
# Parse initial data manually to avoid setting field values as lists
initial_data = {k: request.GET[k] for k in request.GET}
form = self.model_form(instance=obj, initial=initial_data)
# Prefilled fields for comments
created_by = request.user
for_device = request.build_absolute_uri().replace('http://', '').replace('https://', '').split('/')[2].split('/')[0]
return render(request, self.template_name, {
'obj': obj,
'obj_type': self.model._meta.verbose_name,
'form': form,
'return_url': self.get_return_url(request, obj),
'created_by': created_by,
'for_device': for_device,
})

View File

@ -120,6 +120,7 @@ EMAIL_SUBJECT_PREFIX = '[NetBox] '
# Installed applications
INSTALLED_APPS = (
'activity',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',

View File

@ -25,6 +25,8 @@ schema_view = get_schema_view(
_patterns = [
url(r'^activity/', include('activity.urls', namespace='activity'), name='activity'),
# Base views
url(r'^$', HomeView.as_view(), name='home'),
url(r'^search/$', SearchView.as_view(), name='search'),

View File

@ -0,0 +1,83 @@
{% extends '_base.html' %}
{% load form_helpers %}
{% block title %}Add a new comment{% endblock %}
{% block content %}
<form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h3>Add a new comment</h3>
{% block tabs %}{% endblock %}
{% if form.non_field_errors %}
<div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div>
<div class="panel-body">
{{ form.non_field_errors }}
</div>
</div>
{% endif %}
{% block form %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Comment</strong></div>
<div class="panel-body">
<!-- Hide all fields except the comment body-->
<style>
.form-group > div.col-md-9, .form-group > label {
height: 0;
padding: 0;
margin: 0;
overflow: hidden;
visibility: hidden;
}
</style>
{% render_form form %}
<!-- Pre-filled hidden fields -->
<div class="form-group">
<label class="col-md-3 control-label required" for="id_for_device">for_device</label>
<div class="col-md-9">
<select name="for_device" required="" class="form-control" id="id_for_device" placeholder="for_device">
<option value="{{ for_device }}" selected=""></option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required" for="id_created_by">Created by</label>
<div class="col-md-9">
<select name="created_by" required="" placeholder="Created by" class="form-control" id="id_created_by">
<option value="{{ user.pk }}" selected></option>
</select>
</div>
</div>
<!-- End of pre-filled hidden fields -->
</div>
</div>
{% endblock %}
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3 text-right">
{% block buttons %}
{% if obj.pk %}
<button type="submit" name="_update" class="btn btn-primary">Update</button>
{% else %}
<button type="submit" name="_create" class="btn btn-primary">Create</button>
{% endif %}
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
{% endblock %}
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends 'utilities/obj_delete.html' %}
{% block message %}
<p>Are you sure you want to delete the comment below?</p>
<pre>{{ obj.body }}</pre>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}{{ device }} - Activity{% endblock %}
{% block content %}
{% include 'dcim/inc/device_header.html' with active_tab='activity' %}
<div class="row">
<div class="col-md-10">
{% if logItems.count == 0 %}
<h5>No activity yet.</h5>
{% endif %}
{% for logItem in logItems %}
<br>
{% if perms.activity.delete_logitem %}
<a class="btn btn-danger pull-right btn-xs" href="delete/{{ logItem.pk }}"><span class="glyphicon glyphicon-trash"></span></a>
{% endif %}
<p>{% if logItem.created_by is not None %}<b style="font-size:16px;margin-bottom:0">{{ logItem.created_by }}</b>{% endif %}{% if logItem.created_by == None %}A deleted user{% endif %} posted a comment at <b>{{ logItem.created_at }}</b>:<br><br>{{ logItem.body|gfm }}</p>
<br>
<hr>
{% endfor %}
</div>
<div class="col-md-2">
{% if perms.activity.add_logitem %}
<a class="btn btn-primary pull-right" href="add"><span class="fa fa-plus"></span> Add a comment</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -46,6 +46,9 @@
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}>
<a href="{% url 'dcim:device' pk=device.pk %}">Info</a>
</li>
<li role="presentation"{% if active_tab == 'activity' %} class="active"{% endif %}>
<a href="/activity/{{ device.pk }}">Activity</a>
</li>
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
</li>

View File

@ -104,7 +104,7 @@
</li>
</ul>
</li>
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/virtual-chassis,/dcim/manufacturers/,/dcim/platforms/,-connections/,/dcim/inventory-items/' %} active{% endif %}">
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/virtual-chassis,/dcim/manufacturers/,/dcim/platforms/,-connections/,/dcim/inventory-items/,/activity/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">Devices</li>