diff --git a/base_requirements.txt b/base_requirements.txt index 67208ef47..305df4dba 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -19,9 +19,8 @@ django-filter django-htmx # Modified Preorder Tree Traversal (recursive nesting of objects) -# Pinned to 0.14.0; 0.15.0 requires Python 3.9+ # https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst -django-mptt==0.14.0 +django-mptt # Context managers for PostgreSQL advisory locks # https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt diff --git a/contrib/netbox.service b/contrib/netbox.service index 3cd02d988..8c602fa5b 100644 --- a/contrib/netbox.service +++ b/contrib/netbox.service @@ -12,8 +12,12 @@ Group=netbox PIDFile=/var/tmp/netbox.pid WorkingDirectory=/opt/netbox +# Remove the following line if using uWSGI instead of Gunicorn ExecStart=/opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi +# Uncomment the following line if using uWSGI instead of Gunicorn +#ExecStart=/opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini + Restart=on-failure RestartSec=30 PrivateTmp=true diff --git a/contrib/nginx.conf b/contrib/nginx.conf index 34821cd52..31d026e0d 100644 --- a/contrib/nginx.conf +++ b/contrib/nginx.conf @@ -14,10 +14,20 @@ server { } location / { + # Remove these lines if using uWSGI instead of Gunicorn proxy_pass http://127.0.0.1:8001; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; + + # Uncomment these lines if using uWSGI instead of Gunicorn + # include uwsgi_params; + # uwsgi_pass 127.0.0.1:8001; + # uwsgi_param Host $host; + # uwsgi_param X-Real-IP $remote_addr; + # uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for; + # uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto; + } } diff --git a/contrib/uwsgi.ini b/contrib/uwsgi.ini new file mode 100644 index 000000000..d64803158 --- /dev/null +++ b/contrib/uwsgi.ini @@ -0,0 +1,18 @@ +[uwsgi] +; bind to the specified UNIX/TCP socket and port (usually localhost) +socket = 127.0.0.1:8001 + +; fail to start if any parameter in the configuration file isn’t explicitly understood by uWSGI. +strict = true + +; re-spawn and pre-fork workers +master = true + +; clear environment on exit +vacuum = true + +; exit if no app can be loaded +need-app = true + +; do not use multiple interpreters +single-interpreter = true diff --git a/docs/installation/4-gunicorn.md b/docs/installation/4a-gunicorn.md similarity index 83% rename from docs/installation/4-gunicorn.md rename to docs/installation/4a-gunicorn.md index 1e8d49453..3aca4ef0e 100644 --- a/docs/installation/4-gunicorn.md +++ b/docs/installation/4a-gunicorn.md @@ -1,10 +1,13 @@ # Gunicorn -Like most Django applications, NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [gunicorn](http://gunicorn.org/) (which is automatically installed with NetBox) for this role, however other WSGI servers are available and should work similarly well. [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) is a popular alternative. +!!! tip + This page provides instructions for setting up the [gunicorn](http://gunicorn.org/) WSGI server. If you plan to use [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) instead, go [here](./4b-uwsgi.md). + +NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [gunicorn](http://gunicorn.org/) (which is automatically installed with NetBox) for this role, however other WSGI servers are available and should work similarly well. ## Configuration -NetBox ships with a default configuration file for gunicorn. To use it, copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten by a future upgrade.) +NetBox ships with a default configuration file for gunicorn. To use it, copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten during a future NetBox upgrade.) ```no-highlight sudo cp /opt/netbox/contrib/gunicorn.py /opt/netbox/gunicorn.py diff --git a/docs/installation/4b-uwsgi.md b/docs/installation/4b-uwsgi.md new file mode 100644 index 000000000..3b7b5f76c --- /dev/null +++ b/docs/installation/4b-uwsgi.md @@ -0,0 +1,104 @@ +# uWSGI + +!!! tip + This page provides instructions for setting up the [uWSGI](https://uwsgi-docs.readthedocs.io/) WSGI server. If you plan to use [gunicorn](http://gunicorn.org/) instead, go [here](./4a-gunicorn.md). + +NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) for this role, however other WSGI servers are available and should work similarly well. + +## Installation + +Activate the Python virtual environment and install the `pyuwsgi` package using pip: + +```no-highlight +source /opt/netbox/venv/bin/activate +pip3 install pyuwsgi +``` + +Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment: + +```no-highlight +sudo sh -c "echo 'pyuwgsi' >> /opt/netbox/local_requirements.txt" +``` + +## Configuration + +NetBox ships with a default configuration file for uWSGI. To use it, copy `/opt/netbox/contrib/uwsgi.ini` to `/opt/netbox/uwsgi.ini`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten during a future NetBox upgrade.) + +```no-highlight +sudo cp /opt/netbox/contrib/uwsgi.ini /opt/netbox/uwsgi.ini +``` + +While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app. + +## systemd Setup + +We'll use systemd to control both uWSGI and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory. + +```no-highlight +sudo cp -v /opt/netbox/contrib/*.service /etc/systemd/system/ +sudo systemctl daemon-reload +``` + +The reference configuration assumes that gunicorn is in use, so we need to update it. Edit the `netbox.service` file to remove the line beginning with `ExecStart=/opt/netbox/venv/bin/gunicorn` and uncomment the line below it. + +!!! warning "Check user & group assignment" + The stock service configuration files packaged with NetBox assume that the service will run with the `netbox` user and group names. If these differ on your installation, be sure to update the service files accordingly. + +Once the configuration file has been saved, reload the service: + +```no-highlight +sudo systemctl daemon-reload +``` + +Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time: + +```no-highlight +sudo systemctl enable --now netbox netbox-rq +``` + +You can use the command `systemctl status netbox` to verify that the WSGI service is running: + +```no-highlight +systemctl status netbox.service +``` + +You should see output similar to the following: + +```no-highlight +● netbox.service - NetBox WSGI Service + Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled) + Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago + Docs: https://docs.netbox.dev/ + Main PID: 1140492 (uwsgi) + Tasks: 19 (limit: 4683) + Memory: 666.2M + CGroup: /system.slice/netbox.service + ├─1061 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini + ├─1976 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini +... +``` + +!!! note + If the NetBox service fails to start, issue the command `journalctl -eu netbox` to check for log messages that may indicate the problem. + +Once you've verified that the WSGI workers are up and running, move on to HTTP server setup. + +## HTTP Server Installation + +For server installation, you will want to follow the NetBox [HTTP Server Setup](5-http-server.md) guide, however after copying the configuration file, you will need to edit the file and change the `location` section to uncomment the uWSGI parameters: + +```no-highlight + location / { + # proxy_pass http://127.0.0.1:8001; + # proxy_set_header X-Forwarded-Host $http_host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-Proto $scheme; + # comment the lines above and uncomment the lines below if using uWSGI + include uwsgi_params; + uwsgi_pass 127.0.0.1:8001; + uwsgi_param Host $host; + uwsgi_param X-Real-IP $remote_addr; + uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for; + uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto; + } +``` diff --git a/docs/installation/5-http-server.md b/docs/installation/5-http-server.md index b81c6d84a..7496d3bf4 100644 --- a/docs/installation/5-http-server.md +++ b/docs/installation/5-http-server.md @@ -35,6 +35,9 @@ Once nginx is installed, copy the nginx configuration file provided by NetBox to sudo cp /opt/netbox/contrib/nginx.conf /etc/nginx/sites-available/netbox ``` +!!! tip "gunicorn vs. uWSGI" + The reference nginx configuration file assumes that gunicorn is in use. If using uWSGI instead, you'll need to remove the gunicorn-specific configuration (lines beginning with `proxy_pass` and `proxy_set_header`) and uncomment the uWSGI section below them before proceeding. + Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created. ```no-highlight diff --git a/mkdocs.yml b/mkdocs.yml index 354c10608..c17354db9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -94,7 +94,8 @@ nav: - 1. PostgreSQL: 'installation/1-postgresql.md' - 2. Redis: 'installation/2-redis.md' - 3. NetBox: 'installation/3-netbox.md' - - 4. Gunicorn: 'installation/4-gunicorn.md' + - 4a. Gunicorn: 'installation/4a-gunicorn.md' + - 4b. uWSGI: 'installation/4b-uwsgi.md' - 5. HTTP Server: 'installation/5-http-server.md' - 6. LDAP (Optional): 'installation/6-ldap.md' - Upgrading NetBox: 'installation/upgrading.md' diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py index 8eecfa8b9..5f64dcc53 100644 --- a/netbox/core/api/schema.py +++ b/netbox/core/api/schema.py @@ -156,8 +156,6 @@ class NetBoxAutoSchema(AutoSchema): remove_fields.append(child_name) if isinstance(child, (ChoiceField, WritableNestedSerializer)): properties[child_name] = None - elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField): - properties[child_name] = None if not properties: return None diff --git a/netbox/core/views.py b/netbox/core/views.py index 400b421d5..b19ab207b 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -25,6 +25,7 @@ from netbox.views import generic from netbox.views.generic.base import BaseObjectView from netbox.views.generic.mixins import TableMixin from utilities.forms import ConfirmationForm +from utilities.htmx import htmx_partial from utilities.query import count_related from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from . import filtersets, forms, tables @@ -320,7 +321,7 @@ class BackgroundTaskListView(TableMixin, BaseRQView): table = self.get_table(data, request, False) # If this is an HTMX request, return only the rendered table HTML - if request.htmx: + if htmx_partial(request): return render(request, 'htmx/table.html', { 'table': table, }) @@ -489,8 +490,8 @@ class WorkerListView(TableMixin, BaseRQView): table = self.get_table(data, request, False) # If this is an HTMX request, return only the rendered table HTML - if request.htmx: - if request.htmx.target != 'object_list': + if htmx_partial(request): + if not request.htmx.target: table.embedded = True # Hide selection checkboxes if 'pk' in table.base_columns: diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py index 0bd8ba824..a5830fa90 100644 --- a/netbox/dcim/api/serializers_/devicetypes.py +++ b/netbox/dcim/api/serializers_/devicetypes.py @@ -1,3 +1,5 @@ +import decimal + from django.utils.translation import gettext as _ from rest_framework import serializers @@ -22,7 +24,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer): max_digits=4, decimal_places=1, label=_('Position (U)'), - min_value=0, + min_value=decimal.Decimal(0), default=1.0 ) subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2468e9236..be3937512 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -20,6 +20,7 @@ from netbox.views import generic from netbox.views.generic.mixins import TableMixin from utilities.data import shallow_compare_dict from utilities.forms import ConfirmationForm, get_field_value +from utilities.htmx import htmx_partial from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.query import count_related from utilities.querydict import normalize_querydict @@ -1224,7 +1225,7 @@ class ScriptResultView(TableMixin, generic.ObjectView): } # If this is an HTMX request, return only the result HTML - if request.htmx: + if htmx_partial(request): response = render(request, 'extras/htmx/script_result.html', context) if job.completed or not job.started: response.status_code = 286 diff --git a/netbox/netbox/api/serializers/base.py b/netbox/netbox/api/serializers/base.py index 4445f62da..448d3dbb4 100644 --- a/netbox/netbox/api/serializers/base.py +++ b/netbox/netbox/api/serializers/base.py @@ -27,9 +27,13 @@ class BaseModelSerializer(serializers.ModelSerializer): self.nested = nested self._requested_fields = fields + # Disable validators for nested objects (which already exist) + if self.nested: + self.validators = [] + # If this serializer is nested but no fields have been specified, # default to using Meta.brief_fields (if set) - if nested and not fields: + if self.nested and not fields: self._requested_fields = getattr(self.Meta, 'brief_fields', None) super().__init__(*args, **kwargs) diff --git a/netbox/netbox/context_processors.py b/netbox/netbox/context_processors.py index 024ca85b5..ce4f8c45e 100644 --- a/netbox/netbox/context_processors.py +++ b/netbox/netbox/context_processors.py @@ -8,9 +8,11 @@ def settings_and_registry(request): """ Expose Django settings and NetBox registry stores in the template context. Example: {{ settings.DEBUG }} """ + user_preferences = request.user.config if request.user.is_authenticated else {} return { 'settings': django_settings, 'config': get_config(), 'registry': registry, - 'preferences': request.user.config if request.user.is_authenticated else {}, + 'preferences': user_preferences, + 'htmx_navigation': user_preferences.get('ui.htmx_navigation', False) == 'true' } diff --git a/netbox/netbox/preferences.py b/netbox/netbox/preferences.py index 9a6fe490c..1414ea850 100644 --- a/netbox/netbox/preferences.py +++ b/netbox/netbox/preferences.py @@ -23,6 +23,14 @@ PREFERENCES = { ), default='light', ), + 'ui.htmx_navigation': UserPreference( + label=_('HTMX Navigation'), + choices=( + ('', _('Disabled')), + ('true', _('Enabled')), + ), + default=False + ), 'locale.language': UserPreference( label=_('Language'), choices=( diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index ba4e585ad..d609f0a18 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -23,6 +23,7 @@ from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields from utilities.forms.bulk_import import BulkImportForm +from utilities.htmx import htmx_partial from utilities.permissions import get_permission_for_model from utilities.views import GetReturnURLMixin, get_viewname from .base import BaseMultiObjectView @@ -161,8 +162,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): table = self.get_table(self.queryset, request, has_bulk_actions) # If this is an HTMX request, return only the rendered table HTML - if request.htmx: - if request.htmx.target != 'object_list': + if htmx_partial(request): + if not request.htmx.target: table.embedded = True # Hide selection checkboxes if 'pk' in table.base_columns: diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 26bd7de65..616867603 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -17,6 +17,7 @@ from extras.signals import clear_events from utilities.error_handlers import handle_protectederror from utilities.exceptions import AbortRequest, PermissionsViolation from utilities.forms import ConfirmationForm, restrict_form_fields +from utilities.htmx import htmx_partial from utilities.permissions import get_permission_for_model from utilities.querydict import normalize_querydict, prepare_cloned_fields from utilities.views import GetReturnURLMixin, get_viewname @@ -138,7 +139,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin): table = self.get_table(table_data, request, has_bulk_actions) # If this is an HTMX request, return only the rendered table HTML - if request.htmx: + if htmx_partial(request): return render(request, 'htmx/table.html', { 'object': instance, 'table': table, @@ -226,7 +227,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): restrict_form_fields(form, request.user) # If this is an HTMX request, return only the rendered form HTML - if request.htmx: + if htmx_partial(request): return render(request, 'htmx/form.html', { 'form': form, }) @@ -482,7 +483,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): instance = self.alter_object(self.queryset.model(), request) # If this is an HTMX request, return only the rendered form HTML - if request.htmx: + if htmx_partial(request): return render(request, 'htmx/form.html', { 'form': form, }) diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index fc6c18218..9678b71e3 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -17,6 +17,7 @@ from netbox.forms import SearchForm from netbox.search import LookupTypes from netbox.search.backends import search_backend from netbox.tables import SearchTable +from utilities.htmx import htmx_partial from utilities.paginator import EnhancedPaginator, get_paginate_count __all__ = ( @@ -104,7 +105,7 @@ class SearchView(View): }).configure(table) # If this is an HTMX request, return only the rendered table HTML - if request.htmx: + if htmx_partial(request): return render(request, 'htmx/table.html', { 'table': table, }) diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index 95ac3c1b1..48923cfbb 100644 Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 07e7d30b4..97796e4fb 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 04cebd4c2..c6443c7df 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/src/htmx.ts b/netbox/project-static/src/htmx.ts index 8d92b60c8..f4092036b 100644 --- a/netbox/project-static/src/htmx.ts +++ b/netbox/project-static/src/htmx.ts @@ -1,11 +1,12 @@ -import { getElements, isTruthy } from './util'; import { initButtons } from './buttons'; +import { initClipboard } from './clipboard' import { initSelects } from './select'; import { initObjectSelector } from './objectSelector'; import { initBootstrap } from './bs'; +import { initMessages } from './messages'; function initDepedencies(): void { - for (const init of [initButtons, initSelects, initObjectSelector, initBootstrap]) { + for (const init of [initButtons, initClipboard, initSelects, initObjectSelector, initBootstrap, initMessages]) { init(); } } @@ -15,16 +16,5 @@ function initDepedencies(): void { * elements. */ export function initHtmx(): void { - for (const element of getElements('[hx-target]')) { - const targetSelector = element.getAttribute('hx-target'); - if (isTruthy(targetSelector)) { - for (const target of getElements(targetSelector)) { - target.addEventListener('htmx:afterSettle', initDepedencies); - } - } - } - - for (const element of getElements('[hx-trigger=load]')) { - element.addEventListener('htmx:afterSettle', initDepedencies); - } + document.addEventListener('htmx:afterSettle', initDepedencies); } diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index 3afaaa9fc..aa7150be7 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -5,6 +5,7 @@ @import '../node_modules/@tabler/core/src/scss/vendor/tom-select'; // Overrides of external libraries +@import 'overrides/bootstrap'; @import 'overrides/tabler'; // Transitional styling to ease migration of templates from NetBox v3.x diff --git a/netbox/project-static/styles/overrides/_bootstrap.scss b/netbox/project-static/styles/overrides/_bootstrap.scss new file mode 100644 index 000000000..005535f48 --- /dev/null +++ b/netbox/project-static/styles/overrides/_bootstrap.scss @@ -0,0 +1,11 @@ +// Disable smooth scrolling for intra-page links +html { + scroll-behavior: auto !important; +} + +// Prevent dropdown menus from being clipped inside responsive tables +.table-responsive { + .dropdown, .btn-group, .btn-group-vertical { + position: static; + } +} diff --git a/netbox/templates/account/preferences.html b/netbox/templates/account/preferences.html index c5a93c162..51f807114 100644 --- a/netbox/templates/account/preferences.html +++ b/netbox/templates/account/preferences.html @@ -6,7 +6,7 @@ {% block title %}{% trans "User Preferences" %}{% endblock %} {% block content %} -
+ {% csrf_token %} {# Built-in preferences #} diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index bb35cd3bf..acaff4295 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -15,6 +15,7 @@ + {# Page title #} {% block title %}{% trans "Home" %}{% endblock %} | NetBox diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index 071396575..931d9c886 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -58,8 +58,48 @@ Blocks: + {# User menu #} - {% include 'inc/user_menu.html' %} + {% if request.user.is_authenticated %} + + {% else %} +
+ + {% trans "Log In" %} + +
+ {% endif %} + {# /User menu #} {# Search box #} @@ -79,6 +119,7 @@ Blocks: {# Page content #}
+
{# Page header #} {% block header %} @@ -122,6 +163,8 @@ Blocks: {% endif %} {# /Bottom banner #} +
+ {# Page footer #}
@@ -173,7 +216,7 @@ Blocks: {# /Footer links #} {# Footer text #} -
    +
diff --git a/netbox/templates/dcim/moduletype/component_templates.html b/netbox/templates/dcim/moduletype/component_templates.html index cf862b2c6..d67c6e8fb 100644 --- a/netbox/templates/dcim/moduletype/component_templates.html +++ b/netbox/templates/dcim/moduletype/component_templates.html @@ -13,13 +13,13 @@