mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-09 00:58:16 -06:00
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:
parent
e5454d6714
commit
9792130819
0
netbox/activity/__init__.py
Normal file
0
netbox/activity/__init__.py
Normal file
5
netbox/activity/apps.py
Normal file
5
netbox/activity/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityConfig(AppConfig):
|
||||||
|
name = 'activity'
|
15
netbox/activity/forms.py
Normal file
15
netbox/activity/forms.py
Normal 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
|
36
netbox/activity/migrations/0001_initial.py
Normal file
36
netbox/activity/migrations/0001_initial.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
netbox/activity/migrations/__init__.py
Normal file
0
netbox/activity/migrations/__init__.py
Normal file
26
netbox/activity/models.py
Normal file
26
netbox/activity/models.py
Normal 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
9
netbox/activity/urls.py
Normal 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
61
netbox/activity/views.py
Normal 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,
|
||||||
|
})
|
@ -120,6 +120,7 @@ EMAIL_SUBJECT_PREFIX = '[NetBox] '
|
|||||||
|
|
||||||
# Installed applications
|
# Installed applications
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
|
'activity',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
|
@ -25,6 +25,8 @@ schema_view = get_schema_view(
|
|||||||
|
|
||||||
_patterns = [
|
_patterns = [
|
||||||
|
|
||||||
|
url(r'^activity/', include('activity.urls', namespace='activity'), name='activity'),
|
||||||
|
|
||||||
# Base views
|
# Base views
|
||||||
url(r'^$', HomeView.as_view(), name='home'),
|
url(r'^$', HomeView.as_view(), name='home'),
|
||||||
url(r'^search/$', SearchView.as_view(), name='search'),
|
url(r'^search/$', SearchView.as_view(), name='search'),
|
||||||
|
83
netbox/templates/activity/addComment.html
Normal file
83
netbox/templates/activity/addComment.html
Normal 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 %}
|
6
netbox/templates/activity/deleteComment.html
Normal file
6
netbox/templates/activity/deleteComment.html
Normal 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 %}
|
34
netbox/templates/activity/displayActivity.html
Normal file
34
netbox/templates/activity/displayActivity.html
Normal 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 %}
|
@ -46,6 +46,9 @@
|
|||||||
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}>
|
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'dcim:device' pk=device.pk %}">Info</a>
|
<a href="{% url 'dcim:device' pk=device.pk %}">Info</a>
|
||||||
</li>
|
</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 %}>
|
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
||||||
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
|
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -104,7 +104,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</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>
|
<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">
|
<ul class="dropdown-menu">
|
||||||
<li class="dropdown-header">Devices</li>
|
<li class="dropdown-header">Devices</li>
|
||||||
|
Loading…
Reference in New Issue
Block a user