nautobot 1.6.21__py3-none-any.whl → 1.6.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

nautobot/core/settings.py CHANGED
@@ -59,6 +59,14 @@ ALLOWED_URL_SCHEMES = (
59
59
  "xmpp",
60
60
  )
61
61
 
62
+ # Banners to display to users. Markdown and limited HTML are allowed.
63
+ if "NAUTOBOT_BANNER_BOTTOM" in os.environ and os.environ["NAUTOBOT_BANNER_BOTTOM"] != "":
64
+ BANNER_BOTTOM = os.environ["NAUTOBOT_BANNER_BOTTOM"]
65
+ if "NAUTOBOT_BANNER_LOGIN" in os.environ and os.environ["NAUTOBOT_BANNER_LOGIN"] != "":
66
+ BANNER_LOGIN = os.environ["NAUTOBOT_BANNER_LOGIN"]
67
+ if "NAUTOBOT_BANNER_TOP" in os.environ and os.environ["NAUTOBOT_BANNER_TOP"] != "":
68
+ BANNER_TOP = os.environ["NAUTOBOT_BANNER_TOP"]
69
+
62
70
  # Base directory wherein all created files (jobs, git repositories, file uploads, static files) will be stored)
63
71
  NAUTOBOT_ROOT = os.getenv("NAUTOBOT_ROOT", os.path.expanduser("~/.nautobot"))
64
72
 
@@ -586,15 +594,15 @@ CONSTANCE_CONFIG = {
586
594
  ],
587
595
  "BANNER_BOTTOM": [
588
596
  "",
589
- "Custom HTML to display in a banner at the bottom of all pages.",
597
+ "Custom Markdown or limited HTML to display in a banner at the bottom of all pages.",
590
598
  ],
591
599
  "BANNER_LOGIN": [
592
600
  "",
593
- "Custom HTML to display in a banner at the top of the login page.",
601
+ "Custom Markdown or limited HTML to display in a banner at the top of the login page.",
594
602
  ],
595
603
  "BANNER_TOP": [
596
604
  "",
597
- "Custom HTML to display in a banner at the top of all pages.",
605
+ "Custom Markdown or limited HTML to display in a banner at the top of all pages.",
598
606
  ],
599
607
  "CHANGELOG_RETENTION": [
600
608
  90,
@@ -869,6 +877,8 @@ BRANDING_FILEPATHS = {
869
877
  "icon_mask": os.getenv(
870
878
  "NAUTOBOT_BRANDING_FILEPATHS_ICON_MASK", None
871
879
  ), # mono-chrome icon used for the mask-icon header
880
+ "css": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_CSS", None), # Custom global CSS
881
+ "javascript": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_JAVASCRIPT", None), # Custom global JavaScript
872
882
  }
873
883
 
874
884
  # Title to use in place of "Nautobot"
@@ -32,6 +32,9 @@
32
32
  <link rel="stylesheet" id="base-theme"
33
33
  href="{% static 'css/base.css' %}?v{{ settings.VERSION }}"
34
34
  onerror="window.location='{% url 'media_failure' %}?filename=css/base.css'">
35
+ {% if settings.BRANDING_FILEPATHS.css %}
36
+ <link rel="stylesheet" id="custom-css" href="{% custom_branding_or_static 'css' None %}">
37
+ {% endif %}
35
38
  <link rel="apple-touch-icon" sizes="180x180" href="{% static 'img/nautobot_icon_180x180.png' %}">
36
39
  <link rel="icon" type="image/png" sizes="32x32" href="{% static 'img/nautobot_icon_32x32.png' %}">
37
40
  <link rel="icon" type="image/png" sizes="16x16" href="{% static 'img/nautobot_icon_16x16.png' %}">
@@ -101,7 +104,7 @@
101
104
  <div class="container-fluid wrapper" {% if is_popup %}style="padding-bottom: 0px;"{% endif %}>
102
105
  {% if "BANNER_TOP"|settings_or_config %}
103
106
  <div class="alert alert-info text-center" role="alert">
104
- {{ "BANNER_TOP"|settings_or_config|safe }}
107
+ {{ "BANNER_TOP"|settings_or_config|render_markdown }}
105
108
  </div>
106
109
  {% endif %}
107
110
  {% if settings.MAINTENANCE_MODE %}
@@ -153,7 +156,7 @@
153
156
  <div class="push"></div>
154
157
  {% if "BANNER_BOTTOM"|settings_or_config %}
155
158
  <div class="alert alert-info text-center banner-bottom" role="alert">
156
- {{ "BANNER_BOTTOM"|settings_or_config|safe }}
159
+ {{ "BANNER_BOTTOM"|settings_or_config|render_markdown }}
157
160
  </div>
158
161
  {% endif %}
159
162
  </div>
@@ -189,6 +192,9 @@
189
192
  {% endif %}
190
193
  {% include 'modals/modal_theme.html' with name='theme'%}
191
194
  <script src="{% static 'js/theme.js' %}"></script>
195
+ {% if settings.BRANDING_FILEPATHS.javascript %}
196
+ <script src="{% custom_branding_or_static 'javascript' None %}"></script>
197
+ {% endif %}
192
198
  {% block javascript %}{% endblock %}
193
199
  </body>
194
200
  </html>
@@ -15,7 +15,7 @@
15
15
  {% if request.user.is_authenticated or not "HIDE_RESTRICTED_UI"|settings_or_config %}
16
16
  {% if "BANNER_TOP"|settings_or_config %}
17
17
  <div class="alert alert-info text-center" role="alert">
18
- {{ "BANNER_TOP"|settings_or_config|safe }}
18
+ {{ "BANNER_TOP"|settings_or_config|render_markdown }}
19
19
  </div>
20
20
  {% endif %}
21
21
  {% endif %}
@@ -40,7 +40,7 @@
40
40
  {% if request.user.is_authenticated or not "HIDE_RESTRICTED_UI"|settings_or_config %}
41
41
  {% if "BANNER_BOTTOM"|settings_or_config %}
42
42
  <div class="alert alert-info text-center banner-bottom" role="alert">
43
- {{ "BANNER_BOTTOM"|settings_or_config|safe }}
43
+ {{ "BANNER_BOTTOM"|settings_or_config|render_markdown }}
44
44
  </div>
45
45
  {% endif %}
46
46
  {% endif %}
@@ -36,6 +36,9 @@ add "&raw" to the end of the URL within a browser.
36
36
  <link rel="stylesheet"
37
37
  href="{% static 'css/base.css' %}?v{{ settings.VERSION }}"
38
38
  onerror="window.location='{% url 'media_failure' %}?filename=css/base.css'">
39
+ {% if settings.BRANDING_FILEPATHS.css %}
40
+ <link rel="stylesheet" id="custom-css" href="{% custom_branding_or_static 'css' None %}">
41
+ {% endif %}
39
42
  <link rel="apple-touch-icon" sizes="180x180" href="{% custom_branding_or_static 'icon_180' 'img/nautobot_icon_180x180.png' %}">
40
43
  <link rel="icon" type="image/png" sizes="32x32" href="{% custom_branding_or_static 'icon_32' 'img/nautobot_icon_32x32.png' %}">
41
44
  <link rel="icon" type="image/png" sizes="16x16" href="{% custom_branding_or_static 'icon_16' 'img/nautobot_icon_16x16.png' %}">
@@ -52,3 +52,6 @@
52
52
  dropdown.removeClass('edge');
53
53
  })
54
54
  </script>
55
+ {% if settings.BRANDING_FILEPATHS.javascript %}
56
+ <script src="{% custom_branding_or_static 'javascript' None %}"></script>
57
+ {% endif %}
@@ -26,6 +26,9 @@
26
26
  <link rel="stylesheet" id="base-theme"
27
27
  href="{% static 'css/base.css' %}?v{{ settings.VERSION }}"
28
28
  onerror="window.location='{% url 'media_failure' %}?filename=css/base.css'">
29
+ {% if settings.BRANDING_FILEPATHS.css %}
30
+ <link rel="stylesheet" id="custom-css" href="{% custom_branding_or_static 'css' None %}">
31
+ {% endif %}
29
32
  <link rel="apple-touch-icon" sizes="180x180" href="{% custom_branding_or_static 'icon_180' 'img/nautobot_icon_180x180.png' %}">
30
33
  <link rel="icon" type="image/png" sizes="32x32" href="{% custom_branding_or_static 'icon_32' 'img/nautobot_icon_32x32.png' %}">
31
34
  <link rel="icon" type="image/png" sizes="16x16" href="{% custom_branding_or_static 'icon_16' 'img/nautobot_icon_16x16.png' %}">
@@ -53,8 +53,8 @@
53
53
  <div class="row" style="margin-top: {% if 'BANNER_LOGIN'|settings_or_config %}100{% else %}150{% endif %}px;">
54
54
  <div class="col-sm-4 col-sm-offset-4">
55
55
  {% if "BANNER_LOGIN"|settings_or_config %}
56
- <div style="margin-bottom: 25px">
57
- {{ "BANNER_LOGIN"|settings_or_config|safe }}
56
+ <div class="alert alert-info text-center" role="alert">
57
+ {{ "BANNER_LOGIN"|settings_or_config|render_markdown }}
58
58
  </div>
59
59
  {% endif %}
60
60
  {% if form.non_field_errors %}
@@ -263,6 +263,8 @@ SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "{{ secret_key }}")
263
263
  # "icon_mask": os.getenv(
264
264
  # "NAUTOBOT_BRANDING_FILEPATHS_ICON_MASK", None
265
265
  # ), # mono-chrome icon used for the mask-icon header
266
+ # "css": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_CSS", None), # Custom global CSS
267
+ # "javascript": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_JAVASCRIPT", None), # Custom global JavaScript
266
268
  # }
267
269
 
268
270
  # Prepended to CSV, YAML and export template filenames (i.e. `nautobot_device.yml`)
@@ -99,6 +99,39 @@ class HomeViewTestCase(TestCase):
99
99
  response_content = response.content.decode(response.charset).replace("\n", "")
100
100
  self.assertNotRegex(response_content, footer_hostname_version_pattern)
101
101
 
102
+ def test_banners_markdown(self):
103
+ url = reverse("home")
104
+ with override_settings(
105
+ BANNER_TOP="# Hello world",
106
+ BANNER_BOTTOM="[info](https://nautobot.com)",
107
+ ):
108
+ response = self.client.get(url)
109
+ self.assertInHTML("<h1>Hello world</h1>", response.content.decode(response.charset))
110
+ self.assertInHTML(
111
+ '<a href="https://nautobot.com" rel="noopener noreferrer">info</a>',
112
+ response.content.decode(response.charset),
113
+ )
114
+
115
+ with override_settings(BANNER_LOGIN="_Welcome to Nautobot!_"):
116
+ self.client.logout()
117
+ response = self.client.get(reverse("login"))
118
+ self.assertInHTML("<em>Welcome to Nautobot!</em>", response.content.decode(response.charset))
119
+
120
+ def test_banners_no_xss(self):
121
+ url = reverse("home")
122
+ with override_settings(
123
+ BANNER_TOP='<script>alert("Hello from above!");</script>',
124
+ BANNER_BOTTOM='<script>alert("Hello from below!");</script>',
125
+ ):
126
+ response = self.client.get(url)
127
+ self.assertNotIn("Hello from above", response.content.decode(response.charset))
128
+ self.assertNotIn("Hello from below", response.content.decode(response.charset))
129
+
130
+ with override_settings(BANNER_LOGIN='<script>alert("Welcome to Nautobot!");</script>'):
131
+ self.client.logout()
132
+ response = self.client.get(reverse("login"))
133
+ self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset))
134
+
102
135
 
103
136
  @override_settings(BRANDING_TITLE="Nautobot")
104
137
  class SearchFieldsTestCase(TestCase):
@@ -27,6 +27,7 @@ from nautobot.extras.models import (
27
27
  ConfigContextSchema,
28
28
  ExportTemplate,
29
29
  GitRepository,
30
+ Job,
30
31
  JobLogEntry,
31
32
  JobResult,
32
33
  Secret,
@@ -119,13 +120,13 @@ class GitTest(TransactionTestCase):
119
120
 
120
121
  def populate_repo(self, path, url, *args, **kwargs):
121
122
  os.makedirs(path)
122
- # TODO(Glenn): populate Jobs as well?
123
123
  os.makedirs(os.path.join(path, "config_contexts"))
124
124
  os.makedirs(os.path.join(path, "config_contexts", "devices"))
125
125
  os.makedirs(os.path.join(path, "config_contexts", "locations"))
126
126
  os.makedirs(os.path.join(path, "config_context_schemas"))
127
127
  os.makedirs(os.path.join(path, "export_templates", "dcim", "device"))
128
128
  os.makedirs(os.path.join(path, "export_templates", "ipam", "vlan"))
129
+ os.makedirs(os.path.join(path, "jobs"))
129
130
 
130
131
  with open(os.path.join(path, "config_contexts", "context.yaml"), "w") as fd:
131
132
  yaml.dump(
@@ -167,6 +168,19 @@ class GitTest(TransactionTestCase):
167
168
  with open(os.path.join(path, "export_templates", "ipam", "vlan", "template.j2"), "w") as fd:
168
169
  fd.write("{% for vlan in queryset %}\n{{ vlan.name }}\n{% endfor %}")
169
170
 
171
+ with open(os.path.join(path, "jobs", "__init__.py"), "w") as fd:
172
+ pass
173
+
174
+ with open(os.path.join(path, "jobs", "job.py"), "w") as fd:
175
+ fd.write(
176
+ """\
177
+ from nautobot.extras.jobs import Job
178
+
179
+ class MyJob(Job):
180
+ def run(self, data, commit):
181
+ pass"""
182
+ )
183
+
170
184
  return mock.DEFAULT
171
185
 
172
186
  def empty_repo(self, path, url, *args, **kwargs):
@@ -177,6 +191,8 @@ class GitTest(TransactionTestCase):
177
191
  os.remove(os.path.join(path, "export_templates", "dcim", "device", "template.j2"))
178
192
  os.remove(os.path.join(path, "export_templates", "dcim", "device", "template2.html"))
179
193
  os.remove(os.path.join(path, "export_templates", "ipam", "vlan", "template.j2"))
194
+ os.remove(os.path.join(path, "jobs", "__init__.py"))
195
+ os.remove(os.path.join(path, "jobs", "job.py"))
180
196
  return mock.DEFAULT
181
197
 
182
198
  def assert_config_context_schema_record_exists(self, name):
@@ -263,6 +279,11 @@ class GitTest(TransactionTestCase):
263
279
  )
264
280
  self.assertIsNotNone(export_template_vlan)
265
281
 
282
+ def assert_job_exists(self, installed=True):
283
+ """Helper function to assert Job exists."""
284
+ job = Job.objects.get(git_repository=self.repo, job_class_name="MyJob")
285
+ self.assertEqual(job.installed, installed)
286
+
266
287
  def test_pull_git_repository_and_refresh_data_with_no_data(self, MockGitRepo):
267
288
  """
268
289
  The pull_git_repository_and_refresh_data job should succeed if the given repo is empty.
@@ -482,6 +503,8 @@ class GitTest(TransactionTestCase):
482
503
  # Case when ContentType.model != ContentType.name, template was added and deleted during sync (#570)
483
504
  self.assert_export_template_vlan_exists("template.j2")
484
505
 
506
+ self.assert_job_exists()
507
+
485
508
  # Now "resync" the repository, but now those files no longer exist in the repository
486
509
  MockGitRepo.side_effect = self.empty_repo
487
510
  # For verisimilitude, don't re-use the old request and job_result
@@ -525,6 +548,9 @@ class GitTest(TransactionTestCase):
525
548
  self.assertIsNone(device.local_context_data)
526
549
  self.assertIsNone(device.local_context_data_owner)
527
550
 
551
+ # Job should still be present in the database but no longer installed
552
+ self.assert_job_exists(installed=False)
553
+
528
554
  def test_pull_git_repository_and_refresh_data_with_bad_data(self, MockGitRepo):
529
555
  """
530
556
  The test_pull_git_repository_and_refresh_data job should gracefully handle bad data in the Git repository
@@ -654,11 +680,11 @@ class GitTest(TransactionTestCase):
654
680
 
655
681
  def populate_repo(path, url):
656
682
  os.makedirs(path)
657
- # Just make config_contexts and export_templates directories as we don't load jobs
658
683
  os.makedirs(os.path.join(path, "config_contexts"))
659
684
  os.makedirs(os.path.join(path, "config_contexts", "devices"))
660
685
  os.makedirs(os.path.join(path, "config_context_schemas"))
661
686
  os.makedirs(os.path.join(path, "export_templates", "dcim", "device"))
687
+ os.makedirs(os.path.join(path, "jobs"))
662
688
  with open(os.path.join(path, "config_contexts", "context.yaml"), "w") as fd:
663
689
  yaml.dump(
664
690
  {
@@ -685,6 +711,18 @@ class GitTest(TransactionTestCase):
685
711
  "w",
686
712
  ) as fd:
687
713
  fd.write("{% for device in queryset %}\n{{ device.name }}\n{% endfor %}")
714
+ with open(os.path.join(path, "jobs", "__init__.py"), "w") as fd:
715
+ pass
716
+ with open(os.path.join(path, "jobs", "job.py"), "w") as fd:
717
+ fd.write(
718
+ """\
719
+ from nautobot.extras.jobs import Job
720
+
721
+ class MyJob(Job):
722
+ def run(self, data, commit):
723
+ pass"""
724
+ )
725
+
688
726
  return mock.DEFAULT
689
727
 
690
728
  MockGitRepo.side_effect = populate_repo
@@ -744,6 +782,8 @@ class GitTest(TransactionTestCase):
744
782
  )
745
783
  self.assertIsNotNone(export_template)
746
784
 
785
+ self.assert_job_exists()
786
+
747
787
  # Now delete the GitRepository
748
788
  self.repo.delete()
749
789
 
@@ -768,6 +808,36 @@ class GitTest(TransactionTestCase):
768
808
  device = Device.objects.get(name=self.device.name)
769
809
  self.assertIsNone(device.local_context_data)
770
810
  self.assertIsNone(device.local_context_data_owner)
811
+ self.assert_job_exists(installed=False)
812
+
813
+ # Now recreate the repository (https://github.com/nautobot/nautobot/issues/2974)
814
+ self.repo = GitRepository(
815
+ name="Test Git Repository",
816
+ slug="test_git_repo",
817
+ remote_url="http://localhost/git.git",
818
+ # Provide everything we know we can provide
819
+ provided_contents=[
820
+ entry.content_identifier for entry in get_datasource_contents("extras.gitrepository")
821
+ ],
822
+ )
823
+ self.repo.save(trigger_resync=False)
824
+
825
+ self.job_result = JobResult.objects.create(
826
+ name=self.repo.name,
827
+ obj_type=ContentType.objects.get_for_model(GitRepository),
828
+ job_id=uuid.uuid4(),
829
+ )
830
+
831
+ pull_git_repository_and_refresh_data(self.repo.pk, self.mock_request, self.job_result.pk)
832
+ self.job_result.refresh_from_db()
833
+
834
+ self.assertEqual(
835
+ self.job_result.status,
836
+ JobResultStatusChoices.STATUS_COMPLETED,
837
+ self.job_result.data,
838
+ )
839
+
840
+ self.assert_job_exists(installed=True)
771
841
 
772
842
  def test_git_dry_run(self, MockGitRepo):
773
843
  with tempfile.TemporaryDirectory() as tempdir:
nautobot/extras/utils.py CHANGED
@@ -9,6 +9,7 @@ import sys
9
9
  from django.apps import apps
10
10
  from django.conf import settings
11
11
  from django.contrib.contenttypes.models import ContentType
12
+ from django.db import IntegrityError
12
13
  from django.db.models import Q
13
14
  from django.template.loader import get_template, TemplateDoesNotExist
14
15
  from django.utils.deconstruct import deconstructible
@@ -365,21 +366,37 @@ def refresh_job_model_from_job_class(job_model_class, job_source, job_class, *,
365
366
  JOB_MAX_NAME_LENGTH,
366
367
  )
367
368
 
368
- job_model, created = job_model_class.objects.get_or_create(
369
- source=job_source[:JOB_MAX_SOURCE_LENGTH],
370
- git_repository=git_repository,
371
- module_name=job_class.__module__[:JOB_MAX_NAME_LENGTH],
372
- job_class_name=job_class.__name__[:JOB_MAX_NAME_LENGTH],
373
- defaults={
374
- "slug": default_slug[:JOB_MAX_SLUG_LENGTH],
375
- "grouping": job_class.grouping[:JOB_MAX_GROUPING_LENGTH],
376
- "name": job_class.name[:JOB_MAX_NAME_LENGTH],
377
- "is_job_hook_receiver": issubclass(job_class, JobHookReceiver),
378
- "is_job_button_receiver": issubclass(job_class, JobButtonReceiver),
379
- "installed": True,
380
- "enabled": False,
381
- },
382
- )
369
+ try:
370
+ job_model, created = job_model_class.objects.get_or_create(
371
+ source=job_source[:JOB_MAX_SOURCE_LENGTH],
372
+ git_repository=git_repository,
373
+ module_name=job_class.__module__[:JOB_MAX_NAME_LENGTH],
374
+ job_class_name=job_class.__name__[:JOB_MAX_NAME_LENGTH],
375
+ defaults={
376
+ "slug": default_slug[:JOB_MAX_SLUG_LENGTH],
377
+ "grouping": job_class.grouping[:JOB_MAX_GROUPING_LENGTH],
378
+ "name": job_class.name[:JOB_MAX_NAME_LENGTH],
379
+ "is_job_hook_receiver": issubclass(job_class, JobHookReceiver),
380
+ "is_job_button_receiver": issubclass(job_class, JobButtonReceiver),
381
+ "installed": True,
382
+ "enabled": False,
383
+ },
384
+ )
385
+ except IntegrityError:
386
+ # can occur in the case where we've deleted a GitRepository, resulting in a Job record with
387
+ # source="git" but git_repository=None, and are now creating a new GitRepository to "re-claim" the Job.
388
+ if git_repository is not None:
389
+ created = False
390
+ job_model = job_model_class.objects.get(
391
+ source=job_source[:JOB_MAX_SOURCE_LENGTH],
392
+ git_repository=None,
393
+ module_name=job_class.__module__[:JOB_MAX_NAME_LENGTH],
394
+ job_class_name=job_class.__name__[:JOB_MAX_NAME_LENGTH],
395
+ )
396
+ job_model.git_repository = git_repository
397
+ job_model.save()
398
+ else:
399
+ raise
383
400
 
384
401
  for field_name in JOB_OVERRIDABLE_FIELDS:
385
402
  # Was this field directly inherited from the job before, or was it overridden in the database?
@@ -5765,8 +5765,7 @@ having to define it on the model yourself.</p>
5765
5765
 
5766
5766
  <details class="quote">
5767
5767
  <summary>Source code in <code>nautobot/extras/utils.py</code></summary>
5768
- <div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-181">181</a></span>
5769
- <span class="normal"><a href="#__codelineno-0-182">182</a></span>
5768
+ <div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-182">182</a></span>
5770
5769
  <span class="normal"><a href="#__codelineno-0-183">183</a></span>
5771
5770
  <span class="normal"><a href="#__codelineno-0-184">184</a></span>
5772
5771
  <span class="normal"><a href="#__codelineno-0-185">185</a></span>
@@ -5782,24 +5781,25 @@ having to define it on the model yourself.</p>
5782
5781
  <span class="normal"><a href="#__codelineno-0-195">195</a></span>
5783
5782
  <span class="normal"><a href="#__codelineno-0-196">196</a></span>
5784
5783
  <span class="normal"><a href="#__codelineno-0-197">197</a></span>
5785
- <span class="normal"><a href="#__codelineno-0-198">198</a></span></pre></div></td><td class="code"><div><pre><span></span><code><a id="__codelineno-0-181" name="__codelineno-0-181"></a><span class="k">def</span> <span class="nf">extras_features</span><span class="p">(</span><span class="o">*</span><span class="n">features</span><span class="p">):</span>
5786
- <a id="__codelineno-0-182" name="__codelineno-0-182"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;</span>
5787
- <a id="__codelineno-0-183" name="__codelineno-0-183"></a><span class="sd"> Decorator used to register extras provided features to a model</span>
5788
- <a id="__codelineno-0-184" name="__codelineno-0-184"></a><span class="sd"> &quot;&quot;&quot;</span>
5789
- <a id="__codelineno-0-185" name="__codelineno-0-185"></a>
5790
- <a id="__codelineno-0-186" name="__codelineno-0-186"></a> <span class="k">def</span> <span class="nf">wrapper</span><span class="p">(</span><span class="n">model_class</span><span class="p">):</span>
5791
- <a id="__codelineno-0-187" name="__codelineno-0-187"></a> <span class="c1"># Initialize the model_features store if not already defined</span>
5792
- <a id="__codelineno-0-188" name="__codelineno-0-188"></a> <span class="k">if</span> <span class="s2">&quot;model_features&quot;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">registry</span><span class="p">:</span>
5793
- <a id="__codelineno-0-189" name="__codelineno-0-189"></a> <span class="n">registry</span><span class="p">[</span><span class="s2">&quot;model_features&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="n">f</span><span class="p">:</span> <span class="n">collections</span><span class="o">.</span><span class="n">defaultdict</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">EXTRAS_FEATURES</span><span class="p">}</span>
5794
- <a id="__codelineno-0-190" name="__codelineno-0-190"></a> <span class="k">for</span> <span class="n">feature</span> <span class="ow">in</span> <span class="n">features</span><span class="p">:</span>
5795
- <a id="__codelineno-0-191" name="__codelineno-0-191"></a> <span class="k">if</span> <span class="n">feature</span> <span class="ow">in</span> <span class="n">EXTRAS_FEATURES</span><span class="p">:</span>
5796
- <a id="__codelineno-0-192" name="__codelineno-0-192"></a> <span class="n">app_label</span><span class="p">,</span> <span class="n">model_name</span> <span class="o">=</span> <span class="n">model_class</span><span class="o">.</span><span class="n">_meta</span><span class="o">.</span><span class="n">label_lower</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;.&quot;</span><span class="p">)</span>
5797
- <a id="__codelineno-0-193" name="__codelineno-0-193"></a> <span class="n">registry</span><span class="p">[</span><span class="s2">&quot;model_features&quot;</span><span class="p">][</span><span class="n">feature</span><span class="p">][</span><span class="n">app_label</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>
5798
- <a id="__codelineno-0-194" name="__codelineno-0-194"></a> <span class="k">else</span><span class="p">:</span>
5799
- <a id="__codelineno-0-195" name="__codelineno-0-195"></a> <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">feature</span><span class="si">}</span><span class="s2"> is not a valid extras feature!&quot;</span><span class="p">)</span>
5800
- <a id="__codelineno-0-196" name="__codelineno-0-196"></a> <span class="k">return</span> <span class="n">model_class</span>
5801
- <a id="__codelineno-0-197" name="__codelineno-0-197"></a>
5802
- <a id="__codelineno-0-198" name="__codelineno-0-198"></a> <span class="k">return</span> <span class="n">wrapper</span>
5784
+ <span class="normal"><a href="#__codelineno-0-198">198</a></span>
5785
+ <span class="normal"><a href="#__codelineno-0-199">199</a></span></pre></div></td><td class="code"><div><pre><span></span><code><a id="__codelineno-0-182" name="__codelineno-0-182"></a><span class="k">def</span> <span class="nf">extras_features</span><span class="p">(</span><span class="o">*</span><span class="n">features</span><span class="p">):</span>
5786
+ <a id="__codelineno-0-183" name="__codelineno-0-183"></a><span class="w"> </span><span class="sd">&quot;&quot;&quot;</span>
5787
+ <a id="__codelineno-0-184" name="__codelineno-0-184"></a><span class="sd"> Decorator used to register extras provided features to a model</span>
5788
+ <a id="__codelineno-0-185" name="__codelineno-0-185"></a><span class="sd"> &quot;&quot;&quot;</span>
5789
+ <a id="__codelineno-0-186" name="__codelineno-0-186"></a>
5790
+ <a id="__codelineno-0-187" name="__codelineno-0-187"></a> <span class="k">def</span> <span class="nf">wrapper</span><span class="p">(</span><span class="n">model_class</span><span class="p">):</span>
5791
+ <a id="__codelineno-0-188" name="__codelineno-0-188"></a> <span class="c1"># Initialize the model_features store if not already defined</span>
5792
+ <a id="__codelineno-0-189" name="__codelineno-0-189"></a> <span class="k">if</span> <span class="s2">&quot;model_features&quot;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">registry</span><span class="p">:</span>
5793
+ <a id="__codelineno-0-190" name="__codelineno-0-190"></a> <span class="n">registry</span><span class="p">[</span><span class="s2">&quot;model_features&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="n">f</span><span class="p">:</span> <span class="n">collections</span><span class="o">.</span><span class="n">defaultdict</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">EXTRAS_FEATURES</span><span class="p">}</span>
5794
+ <a id="__codelineno-0-191" name="__codelineno-0-191"></a> <span class="k">for</span> <span class="n">feature</span> <span class="ow">in</span> <span class="n">features</span><span class="p">:</span>
5795
+ <a id="__codelineno-0-192" name="__codelineno-0-192"></a> <span class="k">if</span> <span class="n">feature</span> <span class="ow">in</span> <span class="n">EXTRAS_FEATURES</span><span class="p">:</span>
5796
+ <a id="__codelineno-0-193" name="__codelineno-0-193"></a> <span class="n">app_label</span><span class="p">,</span> <span class="n">model_name</span> <span class="o">=</span> <span class="n">model_class</span><span class="o">.</span><span class="n">_meta</span><span class="o">.</span><span class="n">label_lower</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;.&quot;</span><span class="p">)</span>
5797
+ <a id="__codelineno-0-194" name="__codelineno-0-194"></a> <span class="n">registry</span><span class="p">[</span><span class="s2">&quot;model_features&quot;</span><span class="p">][</span><span class="n">feature</span><span class="p">][</span><span class="n">app_label</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>
5798
+ <a id="__codelineno-0-195" name="__codelineno-0-195"></a> <span class="k">else</span><span class="p">:</span>
5799
+ <a id="__codelineno-0-196" name="__codelineno-0-196"></a> <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">feature</span><span class="si">}</span><span class="s2"> is not a valid extras feature!&quot;</span><span class="p">)</span>
5800
+ <a id="__codelineno-0-197" name="__codelineno-0-197"></a> <span class="k">return</span> <span class="n">model_class</span>
5801
+ <a id="__codelineno-0-198" name="__codelineno-0-198"></a>
5802
+ <a id="__codelineno-0-199" name="__codelineno-0-199"></a> <span class="k">return</span> <span class="n">wrapper</span>
5803
5803
  </code></pre></div></td></tr></table></div>
5804
5804
  </details>
5805
5805
  </div>
@@ -5460,7 +5460,11 @@ can specify additional apps with ease. Similarly, additional <code>MIDDLEWARE</
5460
5460
  <h2 id="banner_top">BANNER_TOP<a class="headerlink" href="#banner_top" title="Permanent link">&para;</a></h2>
5461
5461
  <h2 id="banner_bottom">BANNER_BOTTOM<a class="headerlink" href="#banner_bottom" title="Permanent link">&para;</a></h2>
5462
5462
  <p>Default: <code>""</code> (Empty string)</p>
5463
- <p>Setting these variables will display custom content in a banner at the top and/or bottom of the page, respectively. HTML is allowed. To replicate the content of the top banner in the bottom banner, set:</p>
5463
+ <p>Environment Variable: <code>NAUTOBOT_BANNER_TOP</code>
5464
+ Environment Variable: <code>NAUTOBOT_BANNER_BOTTOM</code></p>
5465
+ <p>Setting these variables will display custom content in a banner at the top and/or bottom of the page, respectively.</p>
5466
+ <p>Markdown formatting is supported within these messages, as well as <a href="../additional-features/template-filters.html#render_markdown">a limited subset of HTML</a>.</p>
5467
+ <p>To replicate the content of the top banner in the bottom banner, set:</p>
5464
5468
  <div class="highlight"><pre><span></span><code><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="n">BANNER_TOP</span> <span class="o">=</span> <span class="s1">&#39;Your banner text&#39;</span>
5465
5469
  <a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a><span class="n">BANNER_BOTTOM</span> <span class="o">=</span> <span class="n">BANNER_TOP</span>
5466
5470
  </code></pre></div>
@@ -5471,7 +5475,9 @@ can specify additional apps with ease. Similarly, additional <code>MIDDLEWARE</
5471
5475
  <hr />
5472
5476
  <h2 id="banner_login">BANNER_LOGIN<a class="headerlink" href="#banner_login" title="Permanent link">&para;</a></h2>
5473
5477
  <p>Default: <code>""</code> (Empty string)</p>
5474
- <p>This defines custom content to be displayed on the login page above the login form. HTML is allowed.</p>
5478
+ <p>Environment Variable: <code>NAUTOBOT_BANNER_LOGIN</code></p>
5479
+ <p>This defines custom content to be displayed on the login page above the login form.</p>
5480
+ <p>Markdown formatting is supported within this message, as well as <a href="../additional-features/template-filters.html#render_markdown">a limited subset of HTML</a>.</p>
5475
5481
  <div class="admonition version-added">
5476
5482
  <p class="admonition-title">Added in version 1.2.0</p>
5477
5483
  <p>If you do not set a value for this setting in your <code>nautobot_config.py</code>, it can be configured dynamically by an admin user via the Nautobot Admin UI. If you do have a value for this setting in <code>nautobot_config.py</code>, it will override any dynamically configured value.</p>
@@ -5487,9 +5493,11 @@ can specify additional apps with ease. Similarly, additional <code>MIDDLEWARE</
5487
5493
  <a id="__codelineno-2-6" name="__codelineno-2-6" href="#__codelineno-2-6"></a> <span class="s2">&quot;icon_180&quot;</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;NAUTOBOT_BRANDING_FILEPATHS_ICON_180&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">),</span> <span class="c1"># 180x180px icon - used for the apple-touch-icon header</span>
5488
5494
  <a id="__codelineno-2-7" name="__codelineno-2-7" href="#__codelineno-2-7"></a> <span class="s2">&quot;icon_192&quot;</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;NAUTOBOT_BRANDING_FILEPATHS_ICON_192&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">),</span> <span class="c1"># 192x192px icon</span>
5489
5495
  <a id="__codelineno-2-8" name="__codelineno-2-8" href="#__codelineno-2-8"></a> <span class="s2">&quot;icon_mask&quot;</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;NAUTOBOT_BRANDING_FILEPATHS_ICON_MASK&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">),</span> <span class="c1"># mono-chrome icon used for the mask-icon header</span>
5490
- <a id="__codelineno-2-9" name="__codelineno-2-9" href="#__codelineno-2-9"></a><span class="p">}</span>
5496
+ <a id="__codelineno-2-9" name="__codelineno-2-9" href="#__codelineno-2-9"></a> <span class="s2">&quot;css&quot;</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;NAUTOBOT_BRANDING_FILEPATHS_CSS&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">),</span> <span class="c1"># custom global CSS file</span>
5497
+ <a id="__codelineno-2-10" name="__codelineno-2-10" href="#__codelineno-2-10"></a> <span class="s2">&quot;javascript&quot;</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;NAUTOBOT_BRANDING_FILEPATHS_JAVASCRIPT&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">),</span> <span class="c1"># custom global Javascript file</span>
5498
+ <a id="__codelineno-2-11" name="__codelineno-2-11" href="#__codelineno-2-11"></a><span class="p">}</span>
5491
5499
  </code></pre></div>
5492
- <p>A set of filepaths relative to the <a href="#media_root"><code>MEDIA_ROOT</code></a> which locate image assets used for custom branding. Each of these assets takes the place of the corresponding stock Nautobot asset. This allows for instance, providing your own navbar logo and favicon.</p>
5500
+ <p>A set of filepaths relative to the <a href="#media_root"><code>MEDIA_ROOT</code></a> which locate assets used for custom branding of your Nautobot instance. With the exception of <code>css</code> and <code>javascript</code>, which provide the option to add an <em>additional</em> file to Nautobot page content, each of the other assets takes the place of the corresponding stock Nautobot asset. This allows for instance, providing your own navbar logo and favicon.</p>
5493
5501
  <p>These environment variables may be used to specify the values:</p>
5494
5502
  <ul>
5495
5503
  <li><code>NAUTOBOT_BRANDING_FILEPATHS_LOGO</code></li>
@@ -5499,8 +5507,14 @@ can specify additional apps with ease. Similarly, additional <code>MIDDLEWARE</
5499
5507
  <li><code>NAUTOBOT_BRANDING_FILEPATHS_ICON_180</code></li>
5500
5508
  <li><code>NAUTOBOT_BRANDING_FILEPATHS_ICON_192</code></li>
5501
5509
  <li><code>NAUTOBOT_BRANDING_FILEPATHS_ICON_MASK</code></li>
5510
+ <li><code>NAUTOBOT_BRANDING_FILEPATHS_CSS</code></li>
5511
+ <li><code>NAUTOBOT_BRANDING_FILEPATHS_JAVASCRIPT</code></li>
5502
5512
  </ul>
5503
- <p>If a custom image asset is not provided for any of the above options, the stock Nautobot asset is used.</p>
5513
+ <p>If a custom asset is not provided for any of the above options, the stock Nautobot asset is used.</p>
5514
+ <div class="admonition version-added">
5515
+ <p class="admonition-title">Added in version 1.6.22</p>
5516
+ <p>The <code>css</code> and <code>javascript</code> assets were added as options.</p>
5517
+ </div>
5504
5518
  <hr />
5505
5519
  <h2 id="branding_prepended_filename">BRANDING_PREPENDED_FILENAME<a class="headerlink" href="#branding_prepended_filename" title="Permanent link">&para;</a></h2>
5506
5520
  <div class="admonition version-added">