diff --git a/MANIFEST.in b/MANIFEST.in
index b9dca47..faa226a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,3 @@
recursive-include netbox_qrcode/templates *.html
-recursive-include netbox_qrcode/fonts *.ttf
\ No newline at end of file
+recursive-include netbox_qrcode/fonts *.ttf
+recursive-include netbox_qrcode/migrations *.py
\ No newline at end of file
diff --git a/netbox_qrcode/__init__.py b/netbox_qrcode/__init__.py
index 9018347..b07887e 100644
--- a/netbox_qrcode/__init__.py
+++ b/netbox_qrcode/__init__.py
@@ -1,10 +1,9 @@
from extras.plugins import PluginConfig
from .version import __version__
-
class QRCodeConfig(PluginConfig):
name = 'netbox_qrcode'
- verbose_name = 'qrcode'
+ verbose_name = 'QR Code View'
description = 'Generate QR codes for the objects'
version = __version__
author = 'Nikolay Yuzefovich'
diff --git a/netbox_qrcode/admin.py b/netbox_qrcode/admin.py
new file mode 100644
index 0000000..aa32e44
--- /dev/null
+++ b/netbox_qrcode/admin.py
@@ -0,0 +1,14 @@
+from django.contrib import admin
+from .models import QRExtendedDevice, QRExtendedRack, QRExtendedCable
+
+@admin.register(QRExtendedDevice)
+class QRExtendedDeviceAdmin(admin.ModelAdmin):
+ list_display = ("device", "photo")
+
+@admin.register(QRExtendedRack)
+class QRExtendedRackAdmin(admin.ModelAdmin):
+ list_display = ("rack", "photo")
+
+@admin.register(QRExtendedCable)
+class QRExtendedCableAdmin(admin.ModelAdmin):
+ list_display = ("cable", "photo")
diff --git a/netbox_qrcode/filters.py b/netbox_qrcode/filters.py
new file mode 100644
index 0000000..701e41c
--- /dev/null
+++ b/netbox_qrcode/filters.py
@@ -0,0 +1,115 @@
+import django_filters
+from django.db import models
+
+from dcim.models import Site, Region, Rack, Manufacturer, RackRole
+from dcim.choices import DeviceStatusChoices, RackStatusChoices
+
+from utilities.filters import TreeNodeMultipleChoiceFilter
+
+from .models import QRExtendedDevice, QRExtendedRack, QRExtendedCable
+
+class BaseFiltersSet(django_filters.FilterSet):
+
+ q = django_filters.CharFilter(
+ method='search',
+ label='Search',
+ )
+ region = TreeNodeMultipleChoiceFilter(
+ queryset=Region.objects.all(),
+ field_name='site__region',
+ lookup_expr='in',
+ to_field_name='slug',
+ label='Region (slug)',
+ )
+ site = django_filters.ModelMultipleChoiceFilter(
+ field_name='site__slug',
+ queryset=Site.objects.all(),
+ to_field_name='slug',
+ label='Site name (slug)',
+ )
+
+# Recieves QuerySet in Views.py and filters based on form values, returns the resulting filtered queryset back to views.py
+class SearchDeviceFilterSet(BaseFiltersSet):
+
+ status = django_filters.MultipleChoiceFilter(
+ choices=DeviceStatusChoices,
+ null_value=None
+ )
+ device_id = django_filters.ModelMultipleChoiceFilter(
+ queryset= QRExtendedDevice.objects.all(),
+ to_field_name='id',
+ field_name='id',
+ label='Device (ID)',
+ )
+ rack_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='rack',
+ queryset=Rack.objects.all(),
+ label='Rack (ID)',
+ )
+ manufacturer = django_filters.ModelMultipleChoiceFilter(
+ field_name='device_type__manufacturer__slug',
+ queryset=Manufacturer.objects.all(),
+ to_field_name='slug',
+ label='Manufacturer (slug)',
+ )
+
+ class Meta:
+ model = QRExtendedDevice
+ fields = ['id', ]
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ models.Q(name__icontains=value)
+ )
+
+class SearchRackFilterSet(BaseFiltersSet):
+
+ status = django_filters.MultipleChoiceFilter(
+ choices=RackStatusChoices,
+ null_value=None
+ )
+ role = django_filters.ModelMultipleChoiceFilter(
+ field_name='role__slug',
+ queryset=RackRole.objects.all(),
+ to_field_name='slug',
+ label='Role (slug)',
+ )
+
+ class Meta:
+ model = QRExtendedRack
+ fields = ['id', ]
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(
+ models.Q(name__icontains=value)
+ )
+
+class SearchCableFilterSet(django_filters.FilterSet):
+ q = django_filters.CharFilter(
+ method='search',
+ label='Search',
+ )
+
+ class Meta:
+ model = QRExtendedCable
+ fields = ['id', ]
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ return queryset.filter(label__icontains=value)
+
+ def filter_device(self, queryset, name, value):
+ queryset = queryset.filter(
+ models.Q(**{'_termination_a_{}__in'.format(name): value}) |
+ models.Q(**{'_termination_b_{}__in'.format(name): value})
+ )
+ return queryset
+
+
+
+
diff --git a/netbox_qrcode/forms.py b/netbox_qrcode/forms.py
new file mode 100644
index 0000000..a5cbe3d
--- /dev/null
+++ b/netbox_qrcode/forms.py
@@ -0,0 +1,78 @@
+from django import forms
+from dcim.models import Device, Site, Region, Rack, RackRole, Manufacturer
+from dcim.choices import DeviceStatusChoices, RackStatusChoices
+
+from utilities.forms import DynamicModelMultipleChoiceField, StaticSelect2Multiple, DynamicModelChoiceField
+
+
+class BaseFilterForm(forms.Form):
+ q = forms.CharField(
+ required=False,
+ label='Search'
+ )
+ region = DynamicModelMultipleChoiceField(
+ queryset=Region.objects.all(),
+ to_field_name='slug',
+ required=False,
+ initial_params={
+ 'sites': '$site'
+ }
+ )
+ site = DynamicModelMultipleChoiceField(
+ queryset=Site.objects.all(),
+ to_field_name='slug',
+ required=False,
+ query_params={
+ 'region': '$region'
+ }
+ )
+
+
+class SearchFilterFormDevice(BaseFilterForm):
+
+ status = forms.MultipleChoiceField(
+ choices=DeviceStatusChoices,
+ required=False,
+ widget=StaticSelect2Multiple()
+ )
+ device_id = DynamicModelMultipleChoiceField(
+ queryset=Device.objects.all(),
+ to_field_name='id',
+ required=False,
+ label='Device',
+ null_option='None',
+ )
+ rack_id = DynamicModelMultipleChoiceField(
+ queryset=Rack.objects.all(),
+ required=False,
+ label='Rack',
+ null_option='None',
+ )
+ manufacturer = DynamicModelMultipleChoiceField(
+ queryset=Manufacturer.objects.all(),
+ to_field_name='slug',
+ required=False,
+ label='Manufacturer'
+ )
+
+
+class SearchFilterFormRack(BaseFilterForm):
+
+ status = forms.MultipleChoiceField(
+ choices=RackStatusChoices,
+ required=False,
+ widget=StaticSelect2Multiple()
+ )
+ role = DynamicModelChoiceField(
+ queryset=RackRole.objects.all(),
+ to_field_name='slug',
+ required=False,
+ null_option='None',
+ )
+
+
+class SearchFilterFormCable(forms.Form):
+ q = forms.CharField(
+ required=False,
+ label='Search'
+ )
diff --git a/netbox_qrcode/migrations/0001_initial.py b/netbox_qrcode/migrations/0001_initial.py
new file mode 100644
index 0000000..8c7a879
--- /dev/null
+++ b/netbox_qrcode/migrations/0001_initial.py
@@ -0,0 +1,62 @@
+# Generated by Django 3.2 on 2021-06-22 19:53
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('dcim', '0130_sitegroup'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='QRExtendedRack',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('photo', models.ImageField(upload_to='image-attachments/')),
+ ('url', models.URLField(default='')),
+ ('name', models.CharField(blank=True, max_length=100, null=True)),
+ ('status', models.CharField(default='active', max_length=50)),
+ ('rack', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.rack')),
+ ('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.rackrole')),
+ ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.site')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='QRExtendedDevice',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('photo', models.ImageField(upload_to='image-attachments/')),
+ ('url', models.URLField(default='')),
+ ('name', models.CharField(blank=True, max_length=64, null=True)),
+ ('status', models.CharField(default='active', max_length=50)),
+ ('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.device')),
+ ('device_role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.devicerole')),
+ ('device_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.devicetype')),
+ ('rack', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.rack')),
+ ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.site')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='QRExtendedCable',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('photo', models.ImageField(upload_to='image-attachments/')),
+ ('url', models.URLField(default='')),
+ ('cable', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.cable')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/netbox_qrcode/migrations/__init__.py b/netbox_qrcode/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/netbox_qrcode/models.py b/netbox_qrcode/models.py
new file mode 100644
index 0000000..c96243b
--- /dev/null
+++ b/netbox_qrcode/models.py
@@ -0,0 +1,114 @@
+# Extends netbox models with Custom models
+from django.db import models
+from django.urls import reverse
+
+from dcim.choices import DeviceStatusChoices, RackStatusChoices, CableStatusChoices
+from ipam.choices import *
+
+
+# Abstract class which extended objects extend from
+class QRObject(models.Model):
+
+ photo = models.ImageField(upload_to='image-attachments/')
+ url = models.URLField(default='', max_length=200)
+
+ class Meta:
+ abstract = True
+
+# Devices Wrapper
+class QRExtendedDevice(QRObject):
+ device = models.ForeignKey(
+ to="dcim.Device", on_delete=models.CASCADE, null=True)
+
+ name = models.CharField(
+ max_length=64,
+ blank=True,
+ null=True
+ )
+ status = models.CharField(
+ max_length=50,
+ choices=DeviceStatusChoices,
+ default=DeviceStatusChoices.STATUS_ACTIVE
+ )
+ device_type = models.ForeignKey(
+ to='dcim.DeviceType',
+ on_delete=models.CASCADE,
+ related_name='+',
+ blank=True,
+ null=True
+ )
+ device_role = models.ForeignKey(
+ to='dcim.DeviceRole',
+ on_delete=models.CASCADE,
+ related_name='+',
+ blank=True,
+ null=True
+ )
+ site = models.ForeignKey(
+ to='dcim.Site',
+ on_delete=models.CASCADE,
+ related_name='+',
+ blank=True,
+ null=True
+ )
+ rack = models.ForeignKey(
+ to='dcim.Rack',
+ on_delete=models.CASCADE,
+ related_name='+',
+ blank=True,
+ null=True
+ )
+ # Set link for id column in QRDeviceTable to be the return url formatted with device's pk
+ def get_absolute_url(self):
+ return reverse('dcim:device', args=[self.device.pk])
+
+ def get_status_class(self):
+ return DeviceStatusChoices.CSS_CLASSES.get(self.status)
+
+# Racks Wrapper
+class QRExtendedRack(QRObject):
+ rack = models.ForeignKey(
+ to="dcim.Rack", on_delete=models.CASCADE, null=True)
+
+ name = models.CharField(
+ max_length=100,
+ blank=True,
+ null=True
+ )
+ status = models.CharField(
+ max_length=50,
+ choices=RackStatusChoices,
+ default=RackStatusChoices.STATUS_ACTIVE
+ )
+ site = models.ForeignKey(
+ to='dcim.Site',
+ on_delete=models.CASCADE,
+ related_name='+',
+ blank=True,
+ null=True
+ )
+ role = models.ForeignKey(
+ to='dcim.RackRole',
+ on_delete=models.CASCADE,
+ related_name='+',
+ blank=True,
+ null=True,
+ help_text='Functional role'
+ )
+
+ def get_absolute_url(self):
+ return reverse('dcim:rack', args=[self.rack.pk])
+
+ def get_status_class(self):
+ return RackStatusChoices.CSS_CLASSES.get(self.status)
+
+# Cables Wrapper
+class QRExtendedCable(QRObject):
+ cable = models.ForeignKey(
+ to="dcim.Cable", on_delete=models.CASCADE, null=True)
+
+ def get_absolute_url(self):
+ return reverse('dcim:cable', args=[self.cable.pk])
+
+ def get_status_class(self):
+ return CableStatusChoices.CSS_CLASSES.get(self.status)
diff --git a/netbox_qrcode/navigation.py b/netbox_qrcode/navigation.py
new file mode 100644
index 0000000..54db529
--- /dev/null
+++ b/netbox_qrcode/navigation.py
@@ -0,0 +1,9 @@
+from extras.plugins import PluginMenuItem
+
+menu_items = (
+ PluginMenuItem(
+ link='plugins:netbox_qrcode:qrcode_devices',
+ link_text='QR Codes',
+ buttons=()
+ ),
+)
\ No newline at end of file
diff --git a/netbox_qrcode/tables.py b/netbox_qrcode/tables.py
new file mode 100644
index 0000000..166b454
--- /dev/null
+++ b/netbox_qrcode/tables.py
@@ -0,0 +1,83 @@
+import django_tables2 as tables
+from utilities.tables import BaseTable, ChoiceFieldColumn, ColoredLabelColumn, ToggleColumn
+from .models import QRExtendedDevice, QRExtendedRack, QRExtendedCable
+
+# Device Table
+class QRDeviceTables(BaseTable):
+ """Table for displaying Device objects."""
+
+ # Set up hyperlinks to column items
+ pk = ToggleColumn(visible=True)
+ device = tables.LinkColumn()
+ status = ChoiceFieldColumn()
+ device_role = ColoredLabelColumn()
+ device_type = tables.LinkColumn()
+ rack = tables.LinkColumn()
+ site = tables.LinkColumn()
+ id = tables.LinkColumn()
+ url = tables.TemplateColumn(' ', verbose_name = 'QR Code')
+
+ # Netbox base table class, fields display column names/order
+ class Meta(BaseTable.Meta):
+ model = QRExtendedDevice
+ fields = (
+ "pk",
+ "device",
+ "status",
+ "device_role",
+ "device_type",
+ "rack",
+ "site",
+ "id",
+ "photo",
+ "url",
+ )
+
+# Rack Table
+class QRRackTables(BaseTable):
+ """Table for displaying Rack objects."""
+
+ # Set up hyperlinks to column items
+ pk = ToggleColumn(visible=True)
+ rack = tables.LinkColumn()
+ status = ChoiceFieldColumn()
+ site = tables.LinkColumn()
+ role = ColoredLabelColumn()
+ id = tables.LinkColumn()
+ url = tables.TemplateColumn(' ', verbose_name = 'QR Code')
+
+ # Netbox base table class, fields display column names/order
+ class Meta(BaseTable.Meta):
+ model = QRExtendedRack
+ fields = (
+ "pk",
+ "rack",
+ "site",
+ "status",
+ "role",
+ "id",
+ "photo",
+ "url",
+ )
+
+# Cable Table
+class QRCableTables(BaseTable):
+ """Table for displaying Cable objects."""
+
+ # Set up hyperlinks to column items
+ pk = ToggleColumn(visible=True)
+ cable = tables.LinkColumn()
+ # status = ChoiceFieldColumn()
+ id = tables.LinkColumn()
+ url = tables.TemplateColumn(' ', verbose_name = 'QR Code')
+
+ # Netbox base table class, fields display column names/order
+ class Meta(BaseTable.Meta):
+ model = QRExtendedCable
+ fields = (
+ "pk",
+ "cable",
+ "id",
+ "photo",
+ "url",
+ )
\ No newline at end of file
diff --git a/netbox_qrcode/template_content.py b/netbox_qrcode/template_content.py
index 0bf9e64..c83d977 100644
--- a/netbox_qrcode/template_content.py
+++ b/netbox_qrcode/template_content.py
@@ -1,10 +1,8 @@
-
from django.core.exceptions import ObjectDoesNotExist
from extras.plugins import PluginTemplateExtension
from .utilities import get_img_b64, get_qr, get_qr_text, get_concat
-
class QRCode(PluginTemplateExtension):
def x_page(self):
@@ -12,11 +10,11 @@ def x_page(self):
obj = self.context['object']
request = self.context['request']
url = request.build_absolute_uri(obj.get_absolute_url())
- # get object settings
+ # Get object config settings
obj_cfg = config.get(self.model.replace('dcim.', ''))
if obj_cfg is None:
return ''
- # and ovverride default
+ # and override default config
config.update(obj_cfg)
qr_args = {}
@@ -24,7 +22,10 @@ def x_page(self):
if k.startswith('qr_'):
qr_args[k.replace('qr_', '')] = v
+ # Create qr image
qr_img = get_qr(url, **qr_args)
+
+ # Handle qr text if enabled
if config.get('with_text'):
text = []
for text_field in config.get('text_fields', []):
@@ -47,10 +48,16 @@ def x_page(self):
if custom_text:
text.append(custom_text)
text = '\n'.join(text)
+
+ # Create qr text with image size and text
text_img = get_qr_text(qr_img.size, text, config.get('font'))
+
+ # Combine qr image and qr text
qr_with_text = get_concat(qr_img, text_img, config.get('text_location', 'right'))
+ # Convert png to base 64 image
img = get_img_b64(qr_with_text)
+
else:
img = get_img_b64(qr_img)
try:
diff --git a/netbox_qrcode/templates/netbox_qrcode/cables.html b/netbox_qrcode/templates/netbox_qrcode/cables.html
new file mode 100644
index 0000000..e3a67a3
--- /dev/null
+++ b/netbox_qrcode/templates/netbox_qrcode/cables.html
@@ -0,0 +1,42 @@
+
+{% extends 'netbox_qrcode/qrbase.html' %}{% load render_table from django_tables2 %}
+{% block qr_menu_content %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/netbox_qrcode/templates/netbox_qrcode/devices.html b/netbox_qrcode/templates/netbox_qrcode/devices.html
new file mode 100644
index 0000000..350473e
--- /dev/null
+++ b/netbox_qrcode/templates/netbox_qrcode/devices.html
@@ -0,0 +1,42 @@
+
+{% extends 'netbox_qrcode/qrbase.html' %}{% load render_table from django_tables2 %}
+{% block qr_menu_content %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/netbox_qrcode/templates/netbox_qrcode/print.html b/netbox_qrcode/templates/netbox_qrcode/print.html
new file mode 100644
index 0000000..316451e
--- /dev/null
+++ b/netbox_qrcode/templates/netbox_qrcode/print.html
@@ -0,0 +1,84 @@
+{% extends 'base.html' %}
+{% load render_table from django_tables2 %}
+{% block content %}
+
+
+
+
+
+
+
+
+
+ Review Printed {{name}}
+
+
+
+
+
+
+
+
+
+
{% include 'inc/table.html' %}
+
+
+
+
+{% for img in image%}
+
+
+
Page {{ forloop.counter }}
+
+
+
+{% endfor%}
+
+{% endblock %}
diff --git a/netbox_qrcode/templates/netbox_qrcode/qrbase.html b/netbox_qrcode/templates/netbox_qrcode/qrbase.html
new file mode 100644
index 0000000..efaeaaa
--- /dev/null
+++ b/netbox_qrcode/templates/netbox_qrcode/qrbase.html
@@ -0,0 +1,86 @@
+
+{% extends 'base.html' %}
+{% block content %}
+
+
Netbox QR Code Menu
+
+
+
+{% block qr_menu_content %}{% endblock %}
+
+
+
+
+
+
+ {% load form_helpers %}
+ {% include 'inc/search_panel.html' %}
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/netbox_qrcode/templates/netbox_qrcode/qrcode.html b/netbox_qrcode/templates/netbox_qrcode/qrcode.html
index 40a3ed6..6090622 100644
--- a/netbox_qrcode/templates/netbox_qrcode/qrcode.html
+++ b/netbox_qrcode/templates/netbox_qrcode/qrcode.html
@@ -1,20 +1,27 @@
-
- QR Code
-
-
-
-
-
-
\ No newline at end of file
+
+ QR Code
+
+
+
+
diff --git a/netbox_qrcode/templates/netbox_qrcode/racks.html b/netbox_qrcode/templates/netbox_qrcode/racks.html
new file mode 100644
index 0000000..1900ddd
--- /dev/null
+++ b/netbox_qrcode/templates/netbox_qrcode/racks.html
@@ -0,0 +1,42 @@
+
+{% extends 'netbox_qrcode/qrbase.html' %}{% load render_table from django_tables2 %}
+{% block qr_menu_content %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/netbox_qrcode/urls.py b/netbox_qrcode/urls.py
new file mode 100644
index 0000000..b1722a6
--- /dev/null
+++ b/netbox_qrcode/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path
+from . import views
+
+urlpatterns = [
+ path('devices/', views.QRcodeDeviceView.as_view(), name='qrcode_devices'),
+ path('racks/', views.QRcodeRackView.as_view(), name='qrcode_racks'),
+ path('cables/', views.QRcodeCableView.as_view(), name='qrcode_cables'),
+
+ path('devices/print/', views.PrintView.as_view(), name='print_menu'),
+ path('racks/print/', views.PrintView.as_view(), name='print_menu'),
+ path('cables/print/', views.PrintView.as_view(), name='print_menu'),
+]
\ No newline at end of file
diff --git a/netbox_qrcode/utilities.py b/netbox_qrcode/utilities.py
index ad621ce..a23bef2 100644
--- a/netbox_qrcode/utilities.py
+++ b/netbox_qrcode/utilities.py
@@ -28,26 +28,25 @@ def get_img_b64(img):
return str(base64.b64encode(stream.getvalue()), encoding='ascii')
-def get_qr_text(max_size, text, font='TahomaBold'):
- font_size = 56
- tmpimg = Image.new('L', max_size, 'white')
- text_too_large = True
- while text_too_large:
+def get_qr_text(size, text, font='ArialMT', font_size=100):
+ img = Image.new('L', size, 'white')
+ flag = True
+ while flag:
file_path = resource_stream(__name__, 'fonts/{}.ttf'.format(font))
try:
- fnt = ImageFont.truetype(file_path, font_size)
+ fnt = ImageFont.truetype(file_path,font_size)
except Exception:
fnt = ImageFont.load_default()
+ flag = False
- draw = ImageDraw.Draw(tmpimg)
+ draw = ImageDraw.Draw(img)
w, h = draw.textsize(text, font=fnt)
- if w < max_size[0] - 4 and h < max_size[1] - 4:
- text_too_large = False
+ if w < size[0] - 4 and h < size[1] - 4:
+ flag = False
font_size -= 1
+ W, H = size
+ draw.text(((W-w)/2, (H-h)/2), text, font=fnt, fill='black')
- img = Image.new('L', (w, h), 'white')
- draw = ImageDraw.Draw(img)
- draw.text((0, 0), text, font=fnt, fill='black')
return img
@@ -102,3 +101,23 @@ def get_concat(im1, im2, direction='right'):
dst.paste(im2, (im2_x, im2_y))
return dst
+
+
+def get_concat_v(im1, im2):
+ dst = Image.new('L', (im1.width, im1.height + im2.height), 'white')
+ dst.paste(im1, (0, 0))
+ dst.paste(im2, (0, im1.height))
+ return dst
+
+
+def add_print_padding_left(img, padding):
+ blank = Image.new('L', (padding, img.height), 'white')
+ img = get_concat(blank, img)
+ return img
+
+
+def add_print_padding_v(img, padding):
+ blank = Image.new('L', (img.width, padding), 'white')
+ img = get_concat_v(blank, img)
+ img = get_concat_v(img, blank)
+ return img
\ No newline at end of file
diff --git a/netbox_qrcode/version.py b/netbox_qrcode/version.py
index 034f46c..d85b28e 100644
--- a/netbox_qrcode/version.py
+++ b/netbox_qrcode/version.py
@@ -1 +1 @@
-__version__ = "0.0.6"
+__version__ = "0.0.6"
\ No newline at end of file
diff --git a/netbox_qrcode/views.py b/netbox_qrcode/views.py
new file mode 100644
index 0000000..f36cf07
--- /dev/null
+++ b/netbox_qrcode/views.py
@@ -0,0 +1,513 @@
+import requests
+
+from django.shortcuts import redirect, render
+from django.views import View
+from django_tables2 import RequestConfig
+
+from dcim.models import Device, Rack, Cable
+from dcim.tables import DeviceTable, RackTable, CableTable
+
+from . import forms, filters
+
+from .tables import QRDeviceTables, QRRackTables, QRCableTables
+from .models import QRExtendedDevice, QRExtendedRack, QRExtendedCable
+
+from PIL import Image
+from .utilities import *
+
+from django.conf import settings
+
+
+class QRcodeDeviceView(View):
+ template_name = 'netbox_qrcode/devices.html'
+ filterset_device = filters.SearchDeviceFilterSet
+
+ def get(self, request):
+ # Clear all objects in case of duplicate key violation
+ QRExtendedDevice.objects.all().delete()
+
+ base_url = request.build_absolute_uri('/') + 'media/image-attachments/'
+
+ # Find all current Devices and instantiates new models that provide links to photos
+ for device in Device.objects.all().iterator():
+
+ # Create device with resized url
+ url_resized = '{}resized{}.png'.format(base_url, device.name)
+ QRExtendedDevice.objects.get_or_create(
+ id=device.id,
+ device=device,
+ name=device.name,
+ status=device.status,
+ device_type=device.device_type,
+ device_role=device.device_role,
+ site=device.site,
+ rack=device.rack,
+ photo='image-attachments/{}.png'.format(device.name),
+ url=url_resized
+ )
+
+ # Create QuerySets from extended models
+ queryset_device = QRExtendedDevice.objects.all()
+
+ # Filter QuerySets
+ queryset_device = self.filterset_device(
+ request.GET, queryset_device).qs
+
+ # Create Tables for each separate object's querysets
+ table_device = QRDeviceTables(queryset_device)
+
+ # Paginate Tables
+ RequestConfig(request, paginate={
+ "per_page": 50}).configure(table_device)
+
+ # Render html with context
+ return render(request, self.template_name, {
+ 'table_device': table_device,
+ 'filter_form': forms.SearchFilterFormDevice(
+ request.GET,
+ label_suffix=''
+ ),
+ })
+
+ def post(self, request):
+
+ # Get slider values
+ font_size = request.POST.get('font-size-range')
+ box_size = request.POST.get('box-size-range')
+ border_size = request.POST.get('border-size-range')
+
+ # Reload all Object QR Images with input parameters
+ numReloaded = reloadQRImages(request, Device, "devices", int(
+ font_size), int(box_size), int(border_size))
+
+ # Create QuerySets from extended models
+ queryset_device = QRExtendedDevice.objects.all()
+
+ # Filter QuerySets
+ queryset_device = self.filterset_device(
+ request.GET, queryset_device).qs
+
+ # Create Tables for each separate object's querysets
+ table_device = QRDeviceTables(queryset_device)
+
+ # Paginate Tables
+ RequestConfig(request, paginate={
+ "per_page": 50}).configure(table_device)
+
+ # Render html with context
+ return render(request, self.template_name, {
+ 'table_device': table_device,
+ 'filter_form': forms.SearchFilterFormDevice(
+ request.GET,
+ label_suffix=''
+ ),
+ 'successMessage': '
Successfully Reloaded {} Devices
'.format(numReloaded),
+ })
+
+
+class QRcodeRackView(View):
+ template_name = 'netbox_qrcode/racks.html'
+ filterset_rack = filters.SearchRackFilterSet
+
+ def get(self, request):
+ # Clear all objects in case of duplicate key violation
+ QRExtendedRack.objects.all().delete()
+
+ base_url = request.build_absolute_uri('/') + 'media/image-attachments/'
+
+ # Find all current Racks and instantiates new models that provide links to photos
+ for rack in Rack.objects.all().iterator():
+
+ # Create rack with resized url
+ url_resized = '{}resized{}.png'.format(base_url, rack.name)
+ QRExtendedRack.objects.get_or_create(
+ id=rack.id,
+ rack=rack,
+ name=rack.name,
+ status=rack.status,
+ site=rack.site,
+ role=rack.role,
+ photo='image-attachments/{}.png'.format(rack.name),
+ url=url_resized
+ )
+
+ # Create QuerySets from extended models
+ queryset_rack = QRExtendedRack.objects.all()
+
+ # Filter QuerySets
+ queryset_rack = self.filterset_rack(request.GET, queryset_rack).qs
+
+ # Create Tables for each separate object's querysets
+ table_rack = QRRackTables(queryset_rack)
+
+ # Paginate Tables
+ RequestConfig(request, paginate={"per_page": 50}).configure(table_rack)
+
+ # Render html with context
+ return render(request, self.template_name, {
+ 'table_rack': table_rack,
+ 'filter_form': forms.SearchFilterFormRack(
+ request.GET,
+ label_suffix=''
+ ),
+ })
+
+ def post(self, request):
+ # Get slider values
+ font_size = request.POST.get('font-size-range')
+ box_size = request.POST.get('box-size-range')
+ border_size = request.POST.get('border-size-range')
+
+ # Reload all Object QR Images
+ numReloaded = reloadQRImages(request, Rack, "racks", int(
+ font_size), int(box_size), int(border_size))
+
+ # Create QuerySets from extended models
+ queryset_rack = QRExtendedRack.objects.all()
+
+ # Filter QuerySets
+ queryset_rack = self.filterset_rack(request.GET, queryset_rack).qs
+
+ # Create Tables for each separate object's querysets
+ table_rack = QRRackTables(queryset_rack)
+
+ # Paginate Tables
+ RequestConfig(request, paginate={"per_page": 50}).configure(table_rack)
+
+ return render(request, self.template_name, {
+ 'table_rack': table_rack,
+ 'filter_form': forms.SearchFilterFormRack(
+ request.GET,
+ label_suffix=''
+ ),
+ 'successMessage': '
Successfully Reloaded {} Racks
'.format(numReloaded),
+ })
+
+
+class QRcodeCableView(View):
+ template_name = 'netbox_qrcode/cables.html'
+ filterset_cable = filters.SearchCableFilterSet
+
+ def get(self, request):
+ # Clear all objects in case of duplicate key violation
+ QRExtendedCable.objects.all().delete()
+
+ base_url = request.build_absolute_uri('/') + 'media/image-attachments/'
+
+ # Find all current Cables and instantiates new models that provide links to photos
+ for cable in Cable.objects.all().iterator():
+
+ # Create cable with resized url
+ url_resized = '{}resized{}.png'.format(base_url, cable.name)
+ QRExtendedCable.objects.get_or_create(
+ id=cable.id,
+ cable=cable,
+ name=cable.name,
+ photo='image-attachments/{}.png'.format(cable.name),
+ url=url_resized
+ )
+
+ # Create QuerySets from extended models
+ queryset_cable = QRExtendedCable.objects.all()
+
+ # Filter QuerySets
+ queryset_cable = self.filterset_cable(request.GET, queryset_cable).qs
+
+ # Create Tables for each separate object's querysets
+ table_cable = QRCableTables(queryset_cable)
+
+ # Paginate Tables
+ RequestConfig(request, paginate={
+ "per_page": 50}).configure(table_cable)
+
+ # Render html with context
+ return render(request, self.template_name, {
+ 'table_cable': table_cable,
+ 'filter_form': forms.SearchFilterFormCable(
+ request.GET,
+ label_suffix=''
+ ),
+ })
+
+ def post(self, request):
+ # Get slider values
+ font_size = request.POST.get('font-size-range')
+ box_size = request.POST.get('box-size-range')
+ border_size = request.POST.get('border-size-range')
+
+ # Reload all Object QR Images
+ numReloaded = reloadQRImages(request, Cable, "cables", int(
+ font_size), int(box_size), int(border_size))
+
+ # Create QuerySets from extended models
+ queryset_cable = QRExtendedCable.objects.all()
+
+ # Filter QuerySets
+ queryset_cable = self.filterset_cable(request.GET, queryset_cable).qs
+
+ # Create Tables for each separate object's querysets
+ table_cable = QRCableTables(queryset_cable)
+
+ # Paginate Tables
+ RequestConfig(request, paginate={
+ "per_page": 50}).configure(table_cable)
+
+ # Render html with context
+ return render(request, self.template_name, {
+ 'table_cable': table_cable,
+ 'filter_form': forms.SearchFilterFormCable(
+ request.GET,
+ label_suffix=''
+ ),
+ 'successMessage': '
Successfully Reloaded {} Cables
'.format(numReloaded),
+ })
+
+
+# View for when 'Print Selected' Button Pressed
+class PrintView(View):
+
+ template_name = 'netbox_qrcode/print.html'
+
+ # Collect post form content from menu page
+ def post(self, request):
+
+ context = {}
+ pk_list = request.POST.getlist('pk')
+
+ # Get type of netbox object being printed
+ name = request.POST.get('obj_type')
+ context['name'] = name
+
+ # Switch model based on object type
+ obj_dict = {"Devices": Device, "Racks": Rack, "Cables": Cable}
+ Model = obj_dict[name]
+
+ # Switch table based on object type
+ table_dict = {"Devices": DeviceTable,
+ "Racks": RackTable, "Cables": CableTable}
+ object_queryset = Model.objects.filter(pk__in=pk_list)
+ context['table'] = table_dict[name](object_queryset)
+
+ # Number of Images Selected to print
+ image_count = len(pk_list)
+
+ # Set images with or without text should be used and build url
+ without_text = request.POST.get('without_text')
+
+
+ def combine_rows(imageRow, numRows):
+ """
+ Concatenates rows into pages
+ :param imageRow: List of rows of images
+ :param numRows: Number of rows in a page
+ :return: List of pages of images
+ """
+
+ image_pages = []
+
+ final_image = imageRow[0]
+ i = 1
+
+ while i < len(imageRow):
+ final_image = get_concat_v(final_image, imageRow[i])
+ i += 1
+
+ # If page full, push to page image and reset current
+ if (i % numRows) == 0 and i > 0:
+ image_pages.append(get_img_b64(final_image))
+ # If more rows to handle, setup next page image
+ if i < len(imageRow):
+ final_image = imageRow[i]
+ i += 1
+
+ if i % numRows != 0:
+ # Convert and add as page
+ image_pages.append(get_img_b64(final_image))
+
+ return image_pages
+
+ # No text setting option selected
+ if without_text:
+ base_url = request.build_absolute_uri(
+ '/') + 'media/image-attachments/noText'
+ rowSize = 6
+ numRows = 8
+ horizontal_print_padding = 40
+ footer_text_height = 20
+ text_padding = 10
+ context['without_text'] = 1
+ # Print with text enabled and with below settings
+ else:
+ base_url = request.build_absolute_uri(
+ '/') + 'media/image-attachments/'
+ rowSize = 3
+ numRows = 10
+ horizontal_print_padding = 110
+ vertical_print_padding = 5
+ context['without_text'] = 0
+
+ # Multiple selected, account for multi page print
+ if image_count > 0:
+ image_curr = []
+ image_rows = []
+ image_rows_combined = []
+
+ for i in range(image_count):
+
+ obj = Model.objects.get(pk=pk_list[i])
+ url = '{}{}.png'.format(base_url, obj.name)
+ image = Image.open(requests.get(url, stream=True).raw)
+
+ # Append info to bottom of image using user config font if no text QR
+ if without_text:
+ text_img = get_qr_text((image.width, footer_text_height), obj.name, settings.PLUGINS_CONFIG.get(
+ 'netbox_qrcode', {}).get('font'), 200)
+ text_img = add_print_padding_v(text_img, text_padding)
+ image = get_concat_v(image, text_img)
+
+ # Resize text Qr and add vertical padding
+ else:
+ resize_width_height = (162, 88)
+ image = image.resize(resize_width_height)
+ image = add_print_padding_v(image, vertical_print_padding)
+
+ # Append modified image
+ image_curr.append(image)
+
+ # If row full, push to row list and reset current
+ if ((i+1) % rowSize) == 0:
+ image_rows.append(image_curr)
+ image_curr = []
+
+ # Append row list with remaining images if any exists
+ if image_curr:
+ # Make row full for print spacing
+ for i in range(0, rowSize-len(image_curr)):
+ blank = Image.new('L', image.size, 'white')
+ image_curr.append(blank)
+ image_rows.append(image_curr)
+
+ # Loop through each list of rows
+ for row in image_rows:
+
+ # Combine images in single row into one image
+ first_image = row[0]
+ for i in range(1, len(row)):
+ # Add left side padding to all but first image in row and append
+ row[i] = add_print_padding_left(row[i], horizontal_print_padding)
+ first_image = get_concat(first_image, row[i])
+
+ image_rows_combined.append(first_image)
+
+ # Send list of pages to print template
+ context['image'] = combine_rows(image_rows_combined, numRows)
+
+ return render(request, self.template_name, context)
+
+ # No images selected, split current path to remove /print and redirect back to respective object page
+ else:
+ return redirect('/'.join(self.request.path_info.split('/')[:-2]))
+
+
+def reloadQRImages(request, Model, objName, font_size=100, box_size=3, border_size=0):
+ """
+ Creates QRcode image with text, without text, and thumbsized for netbox objects and saves to disk
+ :param request: network request
+ :param Model: netbox object
+ :param objName: netbox object string name
+ :return: number of object images created
+ """
+
+ # Collect User Config and make copy
+ config = settings.PLUGINS_CONFIG.get('netbox_qrcode', {}).copy()
+
+ numReloaded = 0
+ for obj in Model.objects.all().iterator():
+
+ # Check if qrcode already exists
+ image_url = request.build_absolute_uri(
+ '/') + 'media/image-attachments/{}.png'.format(obj.name)
+ rq = requests.get(image_url)
+
+ # Create QR Code only for non-existing or if forced
+ if rq.status_code != 200 or request.POST.get('force-reload-all'):
+ numReloaded += 1
+
+ url = request.build_absolute_uri(
+ '/') + 'dcim/{}/{}'.format(objName, obj.pk)
+
+ # Get object config settings
+ obj_cfg = config.get(objName[:-1])
+ if obj_cfg is None:
+ return ''
+ # and override default config
+ config.update(obj_cfg)
+
+ # Override User Config with print settings
+ printConfig = {}
+ printConfig['qr_box_size'] = box_size
+ printConfig['qr_border'] = border_size
+
+ config.update(printConfig)
+
+ qr_args = {}
+ for k, v in config.items():
+ if k.startswith('qr_'):
+ qr_args[k.replace('qr_', '')] = v
+
+ # Create qr image
+ qr_img = get_qr(url, **qr_args)
+
+ # Handle qr text if enabled
+ if config.get('with_text'):
+ text = []
+ for text_field in config.get('text_fields', []):
+ cfn = None
+ if '.' in text_field:
+ try:
+ text_field, cfn = text_field.split('.')
+ except ValueError:
+ cfn = None
+ if getattr(obj, text_field, None):
+ if cfn:
+ try:
+ if getattr(obj, text_field).get(cfn):
+ text.append('{}'.format(
+ getattr(obj, text_field).get(cfn)))
+ except AttributeError:
+ pass
+ else:
+ text.append('{}'.format(getattr(obj, text_field)))
+ custom_text = config.get('custom_text')
+ if custom_text:
+ text.append(custom_text)
+ text = '\n'.join(text)
+
+ # Create qr text with image size and text
+ text_img = get_qr_text(
+ qr_img.size, text, config.get('font'), font_size)
+
+ # Combine qr image and qr text
+ qr_with_text = get_concat(qr_img, text_img)
+
+ # Save image with text to container with object's first field name
+ text_fields = config.get('text_fields', [])
+ file_path = '/opt/netbox/netbox/media/image-attachments/{}.png'.format(
+ getattr(obj, text_fields[0], 'default'))
+ qr_with_text.save(file_path)
+
+ # Save image without text to container
+ file_path = '/opt/netbox/netbox/media/image-attachments/noText{}.png'.format(
+ getattr(obj, text_fields[0], 'default'))
+ resize_width_height = (90, 90)
+ qr_img = qr_img.resize(resize_width_height)
+ qr_img.save(file_path)
+
+ # Resize final image for thumbnails and save
+ resize_width_height = (120, 50)
+ qr_with_text = qr_with_text.resize(resize_width_height)
+ file_path = '/opt/netbox/netbox/media/image-attachments/resized{}.png'.format(
+ getattr(obj, text_fields[0], 'default'))
+ qr_with_text.save(file_path)
+
+ return numReloaded
diff --git a/setup.py b/setup.py
index 4d9b216..0eeb854 100644
--- a/setup.py
+++ b/setup.py
@@ -33,12 +33,12 @@ def get_version(rel_path):
packages=find_packages(),
include_package_data=True,
package_data={
- '': ['*.ttf'],
- '': ['*.html'],
+ '': ['*.ttf','*.html']
},
install_requires=[
'qrcode',
- 'Pillow'
+ 'Pillow',
+ 'requests',
],
classifiers=[
'Development Status :: 2 - Pre-Alpha',