1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Initial work on : Image attachments

This commit is contained in:
Jeremy Stretch
2017-03-30 21:55:57 -04:00
parent 3ed3e93b25
commit b643939cc4
15 changed files with 214 additions and 12 deletions

@ -15,7 +15,7 @@ from django.db.models import Count, Q, ObjectDoesNotExist
from django.utils.encoding import python_2_unicode_compatible
from circuits.models import Circuit
from extras.models import CustomFieldModel, CustomField, CustomFieldValue
from extras.models import CustomFieldModel, CustomField, CustomFieldValue, ImageAttachment
from extras.rpc import RPC_CLIENTS
from tenancy.models import Tenant
from utilities.fields import ColorField, NullableCharField
@ -375,6 +375,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
help_text='Units are numbered top-to-bottom')
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
images = GenericRelation(ImageAttachment)
objects = RackManager()

@ -3,6 +3,8 @@ from django.conf.urls import url
from ipam.views import ServiceEditView
from secrets.views import secret_add
from extras.views import ImageAttachmentEditView
from .models import Rack
from . import views
@ -49,6 +51,7 @@ urlpatterns = [
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
# Manufacturers
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),

@ -3,9 +3,10 @@ from collections import OrderedDict
from django import forms
from django.contrib.contenttypes.models import ContentType
from utilities.forms import BulkEditForm, LaxURLField
from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
from .models import (
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue,
ImageAttachment,
)
@ -158,3 +159,10 @@ class CustomFieldFilterForm(forms.Form):
for name, field in custom_fields:
field.required = False
self.fields[name] = field
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ImageAttachment
fields = ['name', 'image']

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-30 21:09
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import extras.models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('extras', '0004_topologymap_change_comma_to_semicolon'),
]
operations = [
migrations.CreateModel(
name='ImageAttachment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('image', models.ImageField(height_field=b'image_height', upload_to=extras.models.image_upload, width_field=b'image_width')),
('image_height', models.PositiveSmallIntegerField()),
('image_width', models.PositiveSmallIntegerField()),
('name', models.CharField(blank=True, max_length=50)),
('created', models.DateTimeField(auto_now_add=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
options={
'ordering': ['name'],
},
),
]

@ -359,6 +359,61 @@ class TopologyMap(models.Model):
return graph.pipe(format=img_format)
#
# Image attachments
#
def image_upload(instance, filename):
path = 'image-attachments/'
# Rename the file to the provided name, if any. Attempt to preserve the file extension.
extension = filename.rsplit('.')[-1]
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
filename = '.'.join([instance.name, extension])
elif instance.name:
filename = instance.name
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename)
@python_2_unicode_compatible
class ImageAttachment(models.Model):
"""
An uploaded image which is associated with an object.
"""
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
obj = GenericForeignKey('content_type', 'object_id')
image = models.ImageField(upload_to=image_upload, height_field='image_height', width_field='image_width')
image_height = models.PositiveSmallIntegerField()
image_width = models.PositiveSmallIntegerField()
name = models.CharField(max_length=50, blank=True)
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['name']
def __str__(self):
if self.name:
return self.name
filename = self.image.name.rsplit('/', 1)[-1]
return filename.split('_', 2)[2]
def delete(self, *args, **kwargs):
_name = self.image.name
super(ImageAttachment, self).delete(*args, **kwargs)
# Delete file from disk
self.image.delete(save=False)
# Deleting the file erases its name. We restore the image's filename here in case we still need to reference it
# before the request finishes. (For example, to display a message indicating the ImageAttachment was deleted.)
self.image.name = _name
#
# User actions
#

12
netbox/extras/urls.py Normal file

@ -0,0 +1,12 @@
from django.conf.urls import url
from extras import views
urlpatterns = [
# Image attachments
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
url(r'^image-attachments/(?P<pk>\d+)/delete/$', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
]

30
netbox/extras/views.py Normal file

@ -0,0 +1,30 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.shortcuts import get_object_or_404
from utilities.views import ObjectDeleteView, ObjectEditView
from .forms import ImageAttachmentForm
from .models import ImageAttachment
class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'extras.change_imageattachment'
model = ImageAttachment
form_class = ImageAttachmentForm
def alter_obj(self, imageattachment, request, args, kwargs):
if not imageattachment.pk:
# Assign the parent object based on URL kwargs
model = kwargs.get('model')
imageattachment.obj = get_object_or_404(model, pk=kwargs['object_id'])
return imageattachment
def get_return_url(self, imageattachment):
return imageattachment.obj.get_absolute_url()
class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_imageattachment'
model = ImageAttachment
def get_return_url(self, imageattachment):
return imageattachment.obj.get_absolute_url()

@ -0,0 +1,2 @@
*
!.gitignore

@ -153,6 +153,7 @@ TEMPLATES = [
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.template.context_processors.media',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'utilities.context_processors.settings',
@ -167,19 +168,21 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'en-us'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_ROOT = BASE_DIR + '/static/'
STATIC_URL = '/{}static/'.format(BASE_PATH)
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "project-static"),
)
# Media
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
DATA_UPLOAD_MAX_NUMBER_FIELDS = None

@ -1,6 +1,7 @@
from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin
from django.views.static import serve
from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500
from users.views import login, logout
@ -21,6 +22,7 @@ _patterns = [
# Apps
url(r'^circuits/', include('circuits.urls', namespace='circuits')),
url(r'^dcim/', include('dcim.urls', namespace='dcim')),
url(r'^extras/', include('extras.urls', namespace='extras')),
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
@ -48,6 +50,7 @@ if settings.DEBUG:
import debug_toolbar
_patterns += [
url(r'^__debug__/', include(debug_toolbar.urls)),
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
]
# Prepend BASE_PATH

@ -197,6 +197,55 @@
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Images</strong>
</div>
{% if rack.images.all %}
<table class="table table-hover panel-body">
<tr>
<th>Name</th>
<th>Size</th>
<th>Created</th>
<th></th>
</tr>
{% for attachment in rack.images.all %}
<tr>
<td>
<i class="fa fa-image"></i>
<a href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
</td>
<td>{{ attachment.image.size|filesizeformat }}</td>
<td>{{ attachment.created }}</td>
<td class="text-right">
{% if perms.extras.change_imageattachment %}
<a href="{% url 'extras:imageattachment_edit' pk=attachment.pk %}" class="btn btn-warning btn-xs" title="Edit image">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.extras.delete_imageattachment %}
<a href="{% url 'extras:imageattachment_delete' pk=attachment.pk %}" class="btn btn-danger btn-xs" title="Delete image">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="panel-body">
<span class="text-muted">None</span>
</div>
{% endif %}
{% if perms.extras.add_imageattachment %}
<div class="panel-footer text-right">
<a href="{% url 'dcim:rack_add_image' object_id=rack.pk %}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Attach an image
</a>
</div>
{% endif %}
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Reservations</strong>

@ -2,7 +2,7 @@
{% load form_helpers %}
{% block content %}
<form action="." method="post" class="form form-horizontal">
<form action="." method="post" enctype="multipart/form-data" class="form form-horizontal">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}

@ -425,12 +425,13 @@ class BootstrapMixin(forms.BaseForm):
def __init__(self, *args, **kwargs):
super(BootstrapMixin, self).__init__(*args, **kwargs)
exempt_widgets = [forms.CheckboxInput, forms.ClearableFileInput, forms.FileInput, forms.RadioSelect]
for field_name, field in self.fields.items():
if type(field.widget) not in [type(forms.CheckboxInput()), type(forms.RadioSelect())]:
try:
field.widget.attrs['class'] += ' form-control'
except KeyError:
field.widget.attrs['class'] = 'form-control'
if field.widget.__class__ not in exempt_widgets:
css = field.widget.attrs.get('class', '')
field.widget.attrs['class'] = ' '.join([css, 'form-control']).strip()
if field.required:
field.widget.attrs['required'] = 'required'
if 'placeholder' not in field.widget.attrs:

@ -174,7 +174,7 @@ class ObjectEditView(View):
obj = self.get_object(kwargs)
obj = self.alter_obj(obj, request, args, kwargs)
form = self.form_class(request.POST, instance=obj)
form = self.form_class(request.POST, request.FILES, instance=obj)
if form.is_valid():
obj = form.save(commit=False)

@ -14,6 +14,7 @@ natsort>=5.0.0
ncclient==0.5.2
netaddr==0.7.18
paramiko>=2.0.0
Pillow>=4.0.0
psycopg2>=2.6.1
py-gfm>=0.1.3
pycrypto>=2.6.1