Просмотр исходного кода

Merge pull request #3069 from digitalocean/2647-caching

intial work on #2647 - caching
Jeremy Stretch 6 лет назад
Родитель
Сommit
1877afc760

+ 1 - 0
.travis.yml

@@ -1,6 +1,7 @@
 sudo: required
 sudo: required
 services:
 services:
   - postgresql
   - postgresql
+  - redis-server
 addons:
 addons:
   postgresql: "9.4"
   postgresql: "9.4"
 language: python
 language: python

+ 4 - 0
base_requirements.txt

@@ -18,6 +18,10 @@ django-filter
 # https://github.com/django-mptt/django-mptt
 # https://github.com/django-mptt/django-mptt
 django-mptt
 django-mptt
 
 
+# Django caching using Redis
+# https://github.com/niwinz/django-redis
+django-redis
+
 # Abstraction models for rendering and paginating HTML tables
 # Abstraction models for rendering and paginating HTML tables
 # https://github.com/jieter/django-tables2
 # https://github.com/jieter/django-tables2
 django-tables2
 django-tables2

+ 5 - 0
netbox/circuits/views.py

@@ -1,9 +1,12 @@
+from django.conf import settings
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db import transaction
 from django.db import transaction
 from django.db.models import Count
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django.views.generic import View
 
 
 from extras.models import Graph, GRAPH_TYPE_PROVIDER
 from extras.models import Graph, GRAPH_TYPE_PROVIDER
@@ -32,6 +35,7 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
 class ProviderView(PermissionRequiredMixin, View):
 class ProviderView(PermissionRequiredMixin, View):
     permission_required = 'circuits.view_provider'
     permission_required = 'circuits.view_provider'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, slug):
     def get(self, request, slug):
 
 
         provider = get_object_or_404(Provider, slug=slug)
         provider = get_object_or_404(Provider, slug=slug)
@@ -147,6 +151,7 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView):
 class CircuitView(PermissionRequiredMixin, View):
 class CircuitView(PermissionRequiredMixin, View):
     permission_required = 'circuits.view_circuit'
     permission_required = 'circuits.view_circuit'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
         circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)

+ 22 - 0
netbox/dcim/views.py

@@ -13,6 +13,8 @@ from django.urls import reverse
 from django.utils.html import escape
 from django.utils.html import escape
 from django.utils.http import is_safe_url
 from django.utils.http import is_safe_url
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django.views.generic import View
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
@@ -195,6 +197,7 @@ class SiteListView(PermissionRequiredMixin, ObjectListView):
 class SiteView(PermissionRequiredMixin, View):
 class SiteView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_site'
     permission_required = 'dcim.view_site'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, slug):
     def get(self, request, slug):
 
 
         site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
         site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
@@ -353,6 +356,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
     """
     """
     permission_required = 'dcim.view_rack'
     permission_required = 'dcim.view_rack'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
     def get(self, request):
 
 
         racks = Rack.objects.select_related(
         racks = Rack.objects.select_related(
@@ -392,6 +396,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
 class RackView(PermissionRequiredMixin, View):
 class RackView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_rack'
     permission_required = 'dcim.view_rack'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
         rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
@@ -570,6 +575,7 @@ class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
 class DeviceTypeView(PermissionRequiredMixin, View):
 class DeviceTypeView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_devicetype'
     permission_required = 'dcim.view_devicetype'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         devicetype = get_object_or_404(DeviceType, pk=pk)
         devicetype = get_object_or_404(DeviceType, pk=pk)
@@ -910,6 +916,7 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView):
 class DeviceView(PermissionRequiredMixin, View):
 class DeviceView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_device'
     permission_required = 'dcim.view_device'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         device = get_object_or_404(Device.objects.select_related(
         device = get_object_or_404(Device.objects.select_related(
@@ -991,6 +998,7 @@ class DeviceView(PermissionRequiredMixin, View):
 class DeviceInventoryView(PermissionRequiredMixin, View):
 class DeviceInventoryView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_device'
     permission_required = 'dcim.view_device'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         device = get_object_or_404(Device, pk=pk)
         device = get_object_or_404(Device, pk=pk)
@@ -1012,6 +1020,7 @@ class DeviceInventoryView(PermissionRequiredMixin, View):
 class DeviceStatusView(PermissionRequiredMixin, View):
 class DeviceStatusView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         device = get_object_or_404(Device, pk=pk)
         device = get_object_or_404(Device, pk=pk)
@@ -1025,6 +1034,7 @@ class DeviceStatusView(PermissionRequiredMixin, View):
 class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
 class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         device = get_object_or_404(Device, pk=pk)
         device = get_object_or_404(Device, pk=pk)
@@ -1042,6 +1052,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
 class DeviceConfigView(PermissionRequiredMixin, View):
 class DeviceConfigView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         device = get_object_or_404(Device, pk=pk)
         device = get_object_or_404(Device, pk=pk)
@@ -1279,6 +1290,7 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class InterfaceView(PermissionRequiredMixin, View):
 class InterfaceView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_interface'
     permission_required = 'dcim.view_interface'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         interface = get_object_or_404(Interface, pk=pk)
         interface = get_object_or_404(Interface, pk=pk)
@@ -1499,6 +1511,7 @@ class DeviceBayDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 class DeviceBayPopulateView(PermissionRequiredMixin, View):
 class DeviceBayPopulateView(PermissionRequiredMixin, View):
     permission_required = 'dcim.change_devicebay'
     permission_required = 'dcim.change_devicebay'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         device_bay = get_object_or_404(DeviceBay, pk=pk)
         device_bay = get_object_or_404(DeviceBay, pk=pk)
@@ -1533,6 +1546,7 @@ class DeviceBayPopulateView(PermissionRequiredMixin, View):
 class DeviceBayDepopulateView(PermissionRequiredMixin, View):
 class DeviceBayDepopulateView(PermissionRequiredMixin, View):
     permission_required = 'dcim.change_devicebay'
     permission_required = 'dcim.change_devicebay'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         device_bay = get_object_or_404(DeviceBay, pk=pk)
         device_bay = get_object_or_404(DeviceBay, pk=pk)
@@ -1672,6 +1686,7 @@ class CableListView(PermissionRequiredMixin, ObjectListView):
 class CableView(PermissionRequiredMixin, View):
 class CableView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_cable'
     permission_required = 'dcim.view_cable'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         cable = get_object_or_404(Cable, pk=pk)
         cable = get_object_or_404(Cable, pk=pk)
@@ -1687,6 +1702,7 @@ class CableTraceView(PermissionRequiredMixin, View):
     """
     """
     permission_required = 'dcim.view_cable'
     permission_required = 'dcim.view_cable'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, model, pk):
     def get(self, request, model, pk):
 
 
         obj = get_object_or_404(model, pk=pk)
         obj = get_object_or_404(model, pk=pk)
@@ -1726,6 +1742,7 @@ class CableCreateView(PermissionRequiredMixin, GetReturnURLMixin, View):
 
 
         return super().dispatch(request, *args, **kwargs)
         return super().dispatch(request, *args, **kwargs)
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, *args, **kwargs):
     def get(self, request, *args, **kwargs):
 
 
         # Parse initial data manually to avoid setting field values as lists
         # Parse initial data manually to avoid setting field values as lists
@@ -2042,6 +2059,7 @@ class VirtualChassisCreateView(PermissionRequiredMixin, View):
 class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
 class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
     permission_required = 'dcim.change_virtualchassis'
     permission_required = 'dcim.change_virtualchassis'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
@@ -2110,6 +2128,7 @@ class VirtualChassisDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, View):
 class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, View):
     permission_required = 'dcim.change_virtualchassis'
     permission_required = 'dcim.change_virtualchassis'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
@@ -2164,6 +2183,7 @@ class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, Vi
 class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin, View):
 class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin, View):
     permission_required = 'dcim.change_virtualchassis'
     permission_required = 'dcim.change_virtualchassis'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         device = get_object_or_404(Device, pk=pk, virtual_chassis__isnull=False)
         device = get_object_or_404(Device, pk=pk, virtual_chassis__isnull=False)
@@ -2227,6 +2247,7 @@ class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
 class PowerPanelView(PermissionRequiredMixin, View):
 class PowerPanelView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_powerpanel'
     permission_required = 'dcim.view_powerpanel'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         powerpanel = get_object_or_404(PowerPanel.objects.select_related('site', 'rack_group'), pk=pk)
         powerpanel = get_object_or_404(PowerPanel.objects.select_related('site', 'rack_group'), pk=pk)
@@ -2296,6 +2317,7 @@ class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
 class PowerFeedView(PermissionRequiredMixin, View):
 class PowerFeedView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_powerfeed'
     permission_required = 'dcim.view_powerfeed'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         powerfeed = get_object_or_404(PowerFeed.objects.select_related('power_panel', 'rack'), pk=pk)
         powerfeed = get_object_or_404(PowerFeed.objects.select_related('power_panel', 'rack'), pk=pk)

+ 9 - 0
netbox/extras/views.py

@@ -7,6 +7,8 @@ from django.db.models import Count, Q
 from django.http import Http404
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django.views.generic import View
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
 
 
@@ -41,6 +43,7 @@ class TagListView(ObjectListView):
 
 
 class TagView(View):
 class TagView(View):
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, slug):
     def get(self, request, slug):
 
 
         tag = get_object_or_404(Tag, slug=slug)
         tag = get_object_or_404(Tag, slug=slug)
@@ -108,6 +111,7 @@ class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
 class ConfigContextView(PermissionRequiredMixin, View):
 class ConfigContextView(PermissionRequiredMixin, View):
     permission_required = 'extras.view_configcontext'
     permission_required = 'extras.view_configcontext'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         configcontext = get_object_or_404(ConfigContext, pk=pk)
         configcontext = get_object_or_404(ConfigContext, pk=pk)
@@ -155,6 +159,7 @@ class ObjectConfigContextView(View):
     object_class = None
     object_class = None
     base_template = None
     base_template = None
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         obj = get_object_or_404(self.object_class, pk=pk)
         obj = get_object_or_404(self.object_class, pk=pk)
@@ -187,6 +192,7 @@ class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
 class ObjectChangeView(PermissionRequiredMixin, View):
 class ObjectChangeView(PermissionRequiredMixin, View):
     permission_required = 'extras.view_objectchange'
     permission_required = 'extras.view_objectchange'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         objectchange = get_object_or_404(ObjectChange, pk=pk)
         objectchange = get_object_or_404(ObjectChange, pk=pk)
@@ -209,6 +215,7 @@ class ObjectChangeLogView(View):
     Present a history of changes made to a particular object.
     Present a history of changes made to a particular object.
     """
     """
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, model, **kwargs):
     def get(self, request, model, **kwargs):
 
 
         # Get object my model and kwargs (e.g. slug='foo')
         # Get object my model and kwargs (e.g. slug='foo')
@@ -282,6 +289,7 @@ class ReportListView(PermissionRequiredMixin, View):
     """
     """
     permission_required = 'extras.view_reportresult'
     permission_required = 'extras.view_reportresult'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
     def get(self, request):
 
 
         reports = get_reports()
         reports = get_reports()
@@ -306,6 +314,7 @@ class ReportView(PermissionRequiredMixin, View):
     """
     """
     permission_required = 'extras.view_reportresult'
     permission_required = 'extras.view_reportresult'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, name):
     def get(self, request, name):
 
 
         # Retrieve the Report by "<module>.<report>"
         # Retrieve the Report by "<module>.<report>"

+ 13 - 0
netbox/ipam/views.py

@@ -3,6 +3,8 @@ from django.conf import settings
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db.models import Count, Q
 from django.db.models import Count, Q
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django.views.generic import View
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
 
 
@@ -125,6 +127,7 @@ class VRFListView(PermissionRequiredMixin, ObjectListView):
 class VRFView(PermissionRequiredMixin, View):
 class VRFView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_vrf'
     permission_required = 'ipam.view_vrf'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         vrf = get_object_or_404(VRF.objects.all(), pk=pk)
         vrf = get_object_or_404(VRF.objects.all(), pk=pk)
@@ -319,6 +322,7 @@ class AggregateListView(PermissionRequiredMixin, ObjectListView):
 class AggregateView(PermissionRequiredMixin, View):
 class AggregateView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_aggregate'
     permission_required = 'ipam.view_aggregate'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         aggregate = get_object_or_404(Aggregate, pk=pk)
         aggregate = get_object_or_404(Aggregate, pk=pk)
@@ -456,6 +460,7 @@ class PrefixListView(PermissionRequiredMixin, ObjectListView):
 class PrefixView(PermissionRequiredMixin, View):
 class PrefixView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_prefix'
     permission_required = 'ipam.view_prefix'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         prefix = get_object_or_404(Prefix.objects.select_related(
         prefix = get_object_or_404(Prefix.objects.select_related(
@@ -500,6 +505,7 @@ class PrefixView(PermissionRequiredMixin, View):
 class PrefixPrefixesView(PermissionRequiredMixin, View):
 class PrefixPrefixesView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_prefix'
     permission_required = 'ipam.view_prefix'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
@@ -543,6 +549,7 @@ class PrefixPrefixesView(PermissionRequiredMixin, View):
 class PrefixIPAddressesView(PermissionRequiredMixin, View):
 class PrefixIPAddressesView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_prefix'
     permission_required = 'ipam.view_prefix'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
@@ -643,6 +650,7 @@ class IPAddressListView(PermissionRequiredMixin, ObjectListView):
 class IPAddressView(PermissionRequiredMixin, View):
 class IPAddressView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_ipaddress'
     permission_required = 'ipam.view_ipaddress'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         ipaddress = get_object_or_404(IPAddress.objects.select_related('vrf__tenant', 'tenant'), pk=pk)
         ipaddress = get_object_or_404(IPAddress.objects.select_related('vrf__tenant', 'tenant'), pk=pk)
@@ -726,6 +734,7 @@ class IPAddressAssignView(PermissionRequiredMixin, View):
 
 
         return super().dispatch(request, *args, **kwargs)
         return super().dispatch(request, *args, **kwargs)
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
     def get(self, request):
 
 
         form = forms.IPAddressAssignForm()
         form = forms.IPAddressAssignForm()
@@ -838,6 +847,7 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class VLANGroupVLANsView(PermissionRequiredMixin, View):
 class VLANGroupVLANsView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_vlangroup'
     permission_required = 'ipam.view_vlangroup'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         vlan_group = get_object_or_404(VLANGroup.objects.all(), pk=pk)
         vlan_group = get_object_or_404(VLANGroup.objects.all(), pk=pk)
@@ -888,6 +898,7 @@ class VLANListView(PermissionRequiredMixin, ObjectListView):
 class VLANView(PermissionRequiredMixin, View):
 class VLANView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_vlan'
     permission_required = 'ipam.view_vlan'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         vlan = get_object_or_404(VLAN.objects.select_related(
         vlan = get_object_or_404(VLAN.objects.select_related(
@@ -906,6 +917,7 @@ class VLANView(PermissionRequiredMixin, View):
 class VLANMembersView(PermissionRequiredMixin, View):
 class VLANMembersView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_vlan'
     permission_required = 'ipam.view_vlan'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         vlan = get_object_or_404(VLAN.objects.all(), pk=pk)
         vlan = get_object_or_404(VLAN.objects.all(), pk=pk)
@@ -984,6 +996,7 @@ class ServiceListView(PermissionRequiredMixin, ObjectListView):
 class ServiceView(PermissionRequiredMixin, View):
 class ServiceView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_service'
     permission_required = 'ipam.view_service'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         service = get_object_or_404(Service, pk=pk)
         service = get_object_or_404(Service, pk=pk)

+ 22 - 10
netbox/netbox/configuration.example.py

@@ -25,6 +25,16 @@ DATABASE = {
 # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY
 # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY
 SECRET_KEY = ''
 SECRET_KEY = ''
 
 
+# Redis database settings. The Redis database is used for caching and background processing such as webhooks
+REDIS = {
+    'HOST': 'localhost',
+    'PORT': 6379,
+    'PASSWORD': '',
+    'DATABASE': 0,
+    'DEFAULT_TIMEOUT': 300,
+    'SSL': False,
+}
+
 
 
 #########################
 #########################
 #                       #
 #                       #
@@ -50,6 +60,18 @@ BANNER_LOGIN = ''
 # BASE_PATH = 'netbox/'
 # BASE_PATH = 'netbox/'
 BASE_PATH = ''
 BASE_PATH = ''
 
 
+# The fraction of entries that are culled when CACHE_MAX_ENTRIES is reached. The actual ratio is 1 / CACHE_CULL_FREQUENCY,
+# so set CACHE_CULL_FREQUENCY to 2 to cull half the entries when CACHE_MAX_ENTRIES is reached. This setting should be an
+# integer and defaults to 3
+CACHE_CULL_FREQUENCY = 3
+
+# Max number of entries (unique pages) to store in the cache at a time.
+CACHE_MAX_ENTRIES = 300
+
+# Cache timeout in seconds. Set to `None` to enforce an infinate timeout. Set to 0 to dissable caching by immediatly
+# expiring keys. Defaults to 900 (15 minutes)
+CACHE_TIMEOUT = 900
+
 # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
 # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
 CHANGELOG_RETENTION = 90
 CHANGELOG_RETENTION = 90
 
 
@@ -133,16 +155,6 @@ PAGINATE_COUNT = 50
 # prefer IPv4 instead.
 # prefer IPv4 instead.
 PREFER_IPV4 = False
 PREFER_IPV4 = False
 
 
-# Redis database settings (optional). A Redis database is required only if the webhooks backend is enabled.
-REDIS = {
-    'HOST': 'localhost',
-    'PORT': 6379,
-    'PASSWORD': '',
-    'DATABASE': 0,
-    'DEFAULT_TIMEOUT': 300,
-    'SSL': False,
-}
-
 # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
 # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
 # this setting is derived from the installed location.
 # this setting is derived from the installed location.
 # REPORTS_ROOT = '/opt/netbox/netbox/reports'
 # REPORTS_ROOT = '/opt/netbox/netbox/reports'

+ 30 - 1
netbox/netbox/settings.py

@@ -28,7 +28,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
 
 # Import required configuration parameters
 # Import required configuration parameters
 ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
 ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
-for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
+for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY', 'REDIS']:
     try:
     try:
         globals()[setting] = getattr(configuration, setting)
         globals()[setting] = getattr(configuration, setting)
     except AttributeError:
     except AttributeError:
@@ -44,6 +44,9 @@ BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')
 BASE_PATH = getattr(configuration, 'BASE_PATH', '')
 BASE_PATH = getattr(configuration, 'BASE_PATH', '')
 if BASE_PATH:
 if BASE_PATH:
     BASE_PATH = BASE_PATH.strip('/') + '/'  # Enforce trailing slash only
     BASE_PATH = BASE_PATH.strip('/') + '/'  # Enforce trailing slash only
+CACHE_TIMEOUT = getattr(configuration, 'CACHE_TIMEOUT', 900)
+CACHE_MAX_ENTRIES = getattr(configuration, 'CACHE_MAX_ENTRIES', 300)
+CACHE_CULL_FREQUENCY = getattr(configuration, 'CACHE_CULL_FREQUENCY', 3)
 CHANGELOG_RETENTION = getattr(configuration, 'CHANGELOG_RETENTION', 90)
 CHANGELOG_RETENTION = getattr(configuration, 'CHANGELOG_RETENTION', 90)
 CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
 CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
 CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
 CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
@@ -157,6 +160,7 @@ INSTALLED_APPS = [
     'django.contrib.staticfiles',
     'django.contrib.staticfiles',
     'django.contrib.humanize',
     'django.contrib.humanize',
     'corsheaders',
     'corsheaders',
+    'django_redis',
     'debug_toolbar',
     'debug_toolbar',
     'django_filters',
     'django_filters',
     'django_tables2',
     'django_tables2',
@@ -218,6 +222,31 @@ TEMPLATES = [
     },
     },
 ]
 ]
 
 
+# Caching
+if REDIS_SSL:
+    REDIS_CACHE_CON_STRING = 'rediss://'
+else:
+    REDIS_CACHE_CON_STRING = 'redis://'
+
+if REDIS_PASSWORD:
+    REDIS_CACHE_CON_STRING = '{}@{}'.format(REDIS_PASSWORD, REDIS_CACHE_CON_STRING)
+
+REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(REDIS_CACHE_CON_STRING, REDIS_HOST, REDIS_PORT, REDIS_DATABASE)
+CACHE_BACKEND = 'django_redis.cache.RedisCache'
+
+CACHES = {
+    "default": {
+        "BACKEND": CACHE_BACKEND,
+        "LOCATION": REDIS_CACHE_CON_STRING,
+        'TIMEOUT': CACHE_TIMEOUT,
+        "OPTIONS": {
+            "CLIENT_CLASS": "django_redis.client.DefaultClient",
+            "MAX_ENTRIES": CACHE_MAX_ENTRIES,
+            "CULL_FREQUENCY": CACHE_CULL_FREQUENCY
+        }
+    }
+}
+
 # WSGI
 # WSGI
 WSGI_APPLICATION = 'netbox.wsgi.application'
 WSGI_APPLICATION = 'netbox.wsgi.application'
 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

+ 6 - 0
netbox/netbox/views.py

@@ -1,7 +1,10 @@
 from collections import OrderedDict
 from collections import OrderedDict
 
 
+from django.conf import settings
 from django.db.models import Count, F
 from django.db.models import Count, F
 from django.shortcuts import render
 from django.shortcuts import render
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django.views.generic import View
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.reverse import reverse
 from rest_framework.reverse import reverse
@@ -160,6 +163,7 @@ SEARCH_TYPES = OrderedDict((
 class HomeView(View):
 class HomeView(View):
     template_name = 'home.html'
     template_name = 'home.html'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
     def get(self, request):
 
 
         connected_consoleports = ConsolePort.objects.filter(
         connected_consoleports = ConsolePort.objects.filter(
@@ -219,6 +223,7 @@ class HomeView(View):
 
 
 class SearchView(View):
 class SearchView(View):
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
     def get(self, request):
 
 
         # No query
         # No query
@@ -272,6 +277,7 @@ class APIRootView(APIView):
     def get_view_name(self):
     def get_view_name(self):
         return "API Root"
         return "API Root"
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, format=None):
     def get(self, request, format=None):
 
 
         return Response(OrderedDict((
         return Response(OrderedDict((

+ 3 - 0
netbox/secrets/views.py

@@ -1,5 +1,6 @@
 import base64
 import base64
 
 
+from django.conf import settings
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.auth.decorators import permission_required, login_required
 from django.contrib.auth.decorators import permission_required, login_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -7,6 +8,7 @@ from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.decorators import method_decorator
 from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django.views.generic import View
 
 
 from dcim.models import Device
 from dcim.models import Device
@@ -80,6 +82,7 @@ class SecretListView(PermissionRequiredMixin, ObjectListView):
 class SecretView(PermissionRequiredMixin, View):
 class SecretView(PermissionRequiredMixin, View):
     permission_required = 'secrets.view_secret'
     permission_required = 'secrets.view_secret'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         secret = get_object_or_404(Secret, pk=pk)
         secret = get_object_or_404(Secret, pk=pk)

+ 4 - 0
netbox/tenancy/views.py

@@ -1,6 +1,9 @@
+from django.conf import settings
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db.models import Count
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, render
 from django.shortcuts import get_object_or_404, render
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django.views.generic import View
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
@@ -66,6 +69,7 @@ class TenantListView(PermissionRequiredMixin, ObjectListView):
 class TenantView(PermissionRequiredMixin, View):
 class TenantView(PermissionRequiredMixin, View):
     permission_required = 'tenancy.view_tenant'
     permission_required = 'tenancy.view_tenant'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, slug):
     def get(self, request, slug):
 
 
         tenant = get_object_or_404(Tenant, slug=slug)
         tenant = get_object_or_404(Tenant, slug=slug)

+ 18 - 0
netbox/utilities/api.py

@@ -6,6 +6,8 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import ManyToManyField
 from django.db.models import ManyToManyField
 from django.http import Http404
 from django.http import Http404
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from rest_framework.exceptions import APIException
 from rest_framework.exceptions import APIException
 from rest_framework.permissions import BasePermission
 from rest_framework.permissions import BasePermission
 from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
 from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
@@ -248,6 +250,20 @@ class ModelViewSet(_ModelViewSet):
         # Fall back to the hard-coded serializer class
         # Fall back to the hard-coded serializer class
         return self.serializer_class
         return self.serializer_class
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
+    def list(self, *args, **kwargs):
+        """
+        Call to super to allow for caching
+        """
+        return super().list(*args, **kwargs)
+
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
+    def retrieve(self, *args, **kwargs):
+        """
+        Call to super to allow for caching
+        """
+        return super().retrieve(*args, **kwargs)
+
 
 
 class FieldChoicesViewSet(ViewSet):
 class FieldChoicesViewSet(ViewSet):
     """
     """
@@ -284,9 +300,11 @@ class FieldChoicesViewSet(ViewSet):
                         })
                         })
                 self._fields[key] = choices
                 self._fields[key] = choices
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def list(self, request):
     def list(self, request):
         return Response(self._fields)
         return Response(self._fields)
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def retrieve(self, request, pk):
     def retrieve(self, request, pk):
         if pk not in self._fields:
         if pk not in self._fields:
             raise Http404
             raise Http404

+ 4 - 0
netbox/utilities/views.py

@@ -17,6 +17,8 @@ from django.urls import reverse
 from django.utils.html import escape
 from django.utils.html import escape
 from django.utils.http import is_safe_url
 from django.utils.http import is_safe_url
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.decorators.csrf import requires_csrf_token
 from django.views.decorators.csrf import requires_csrf_token
 from django.views.defaults import ERROR_500_TEMPLATE_NAME
 from django.views.defaults import ERROR_500_TEMPLATE_NAME
 from django.views.generic import View
 from django.views.generic import View
@@ -106,6 +108,7 @@ class ObjectListView(View):
 
 
         return csv_data
         return csv_data
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
     def get(self, request):
 
 
         model = self.queryset.model
         model = self.queryset.model
@@ -713,6 +716,7 @@ class ComponentCreateView(View):
     model_form = None
     model_form = None
     template_name = None
     template_name = None
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         parent = get_object_or_404(self.parent_model, pk=pk)
         parent = get_object_or_404(self.parent_model, pk=pk)

+ 6 - 0
netbox/virtualization/views.py

@@ -1,9 +1,12 @@
+from django.conf import settings
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db import transaction
 from django.db import transaction
 from django.db.models import Count
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django.views.generic import View
 
 
 from dcim.models import Device, Interface
 from dcim.models import Device, Interface
@@ -106,6 +109,7 @@ class ClusterListView(PermissionRequiredMixin, ObjectListView):
 class ClusterView(PermissionRequiredMixin, View):
 class ClusterView(PermissionRequiredMixin, View):
     permission_required = 'virtualization.view_cluster'
     permission_required = 'virtualization.view_cluster'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         cluster = get_object_or_404(Cluster, pk=pk)
         cluster = get_object_or_404(Cluster, pk=pk)
@@ -168,6 +172,7 @@ class ClusterAddDevicesView(PermissionRequiredMixin, View):
     form = forms.ClusterAddDevicesForm
     form = forms.ClusterAddDevicesForm
     template_name = 'virtualization/cluster_add_devices.html'
     template_name = 'virtualization/cluster_add_devices.html'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         cluster = get_object_or_404(Cluster, pk=pk)
         cluster = get_object_or_404(Cluster, pk=pk)
@@ -263,6 +268,7 @@ class VirtualMachineListView(PermissionRequiredMixin, ObjectListView):
 class VirtualMachineView(PermissionRequiredMixin, View):
 class VirtualMachineView(PermissionRequiredMixin, View):
     permission_required = 'virtualization.view_virtualmachine'
     permission_required = 'virtualization.view_virtualmachine'
 
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
     def get(self, request, pk):
 
 
         virtualmachine = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
         virtualmachine = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)

+ 1 - 0
requirements.txt

@@ -3,6 +3,7 @@ django-cors-headers==2.5.2
 django-debug-toolbar==1.11
 django-debug-toolbar==1.11
 django-filter==2.1.0
 django-filter==2.1.0
 django-mptt==0.9.1
 django-mptt==0.9.1
+django-redis==4.5.0
 django-tables2==2.0.6
 django-tables2==2.0.6
 django-taggit==1.1.0
 django-taggit==1.1.0
 django-taggit-serializer==0.1.7
 django-taggit-serializer==0.1.7