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 +13 -3
- nautobot/core/templates/admin/base.html +8 -2
- nautobot/core/templates/base.html +2 -2
- nautobot/core/templates/graphene/graphiql.html +3 -0
- nautobot/core/templates/inc/javascript.html +3 -0
- nautobot/core/templates/inc/media.html +3 -0
- nautobot/core/templates/login.html +2 -2
- nautobot/core/templates/nautobot_config.py.j2 +2 -0
- nautobot/core/tests/test_views.py +33 -0
- nautobot/extras/tests/test_datasources.py +72 -2
- nautobot/extras/utils.py +32 -15
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +20 -20
- nautobot/project-static/docs/configuration/optional-settings.html +19 -5
- nautobot/project-static/docs/release-notes/version-1.6.html +217 -135
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +187 -187
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- {nautobot-1.6.21.dist-info → nautobot-1.6.22.dist-info}/METADATA +1 -1
- {nautobot-1.6.21.dist-info → nautobot-1.6.22.dist-info}/RECORD +23 -23
- {nautobot-1.6.21.dist-info → nautobot-1.6.22.dist-info}/LICENSE.txt +0 -0
- {nautobot-1.6.21.dist-info → nautobot-1.6.22.dist-info}/NOTICE +0 -0
- {nautobot-1.6.21.dist-info → nautobot-1.6.22.dist-info}/WHEEL +0 -0
- {nautobot-1.6.21.dist-info → nautobot-1.6.22.dist-info}/entry_points.txt +0 -0
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|
|
|
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|
|
|
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|
|
|
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|
|
|
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' %}">
|
|
@@ -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
|
|
57
|
-
{{ "BANNER_LOGIN"|settings_or_config|
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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-
|
|
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
|
|
5786
|
-
<a id="__codelineno-0-182" name="__codelineno-0-182"></a><span class="
|
|
5787
|
-
<a id="__codelineno-0-183" name="__codelineno-0-183"></a><span class="
|
|
5788
|
-
<a id="__codelineno-0-184" name="__codelineno-0-184"></a><span class="sd">
|
|
5789
|
-
<a id="__codelineno-0-185" name="__codelineno-0-185"></a>
|
|
5790
|
-
<a id="__codelineno-0-186" name="__codelineno-0-186"></a>
|
|
5791
|
-
<a id="__codelineno-0-187" name="__codelineno-0-187"></a>
|
|
5792
|
-
<a id="__codelineno-0-188" name="__codelineno-0-188"></a> <span class="
|
|
5793
|
-
<a id="__codelineno-0-189" name="__codelineno-0-189"></a>
|
|
5794
|
-
<a id="__codelineno-0-190" name="__codelineno-0-190"></a>
|
|
5795
|
-
<a id="__codelineno-0-191" name="__codelineno-0-191"></a>
|
|
5796
|
-
<a id="__codelineno-0-192" name="__codelineno-0-192"></a>
|
|
5797
|
-
<a id="__codelineno-0-193" name="__codelineno-0-193"></a> <span class="n">
|
|
5798
|
-
<a id="__codelineno-0-194" name="__codelineno-0-194"></a>
|
|
5799
|
-
<a id="__codelineno-0-195" name="__codelineno-0-195"></a>
|
|
5800
|
-
<a id="__codelineno-0-196" name="__codelineno-0-196"></a>
|
|
5801
|
-
<a id="__codelineno-0-197" name="__codelineno-0-197"></a>
|
|
5802
|
-
<a id="__codelineno-0-198" name="__codelineno-0-198"></a>
|
|
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">"""</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"> """</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">"model_features"</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">"model_features"</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">"."</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">"model_features"</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">"</span><span class="si">{</span><span class="n">feature</span><span class="si">}</span><span class="s2"> is not a valid extras feature!"</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">¶</a></h2>
|
|
5461
5461
|
<h2 id="banner_bottom">BANNER_BOTTOM<a class="headerlink" href="#banner_bottom" title="Permanent link">¶</a></h2>
|
|
5462
5462
|
<p>Default: <code>""</code> (Empty string)</p>
|
|
5463
|
-
<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">'Your banner text'</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">¶</a></h2>
|
|
5473
5477
|
<p>Default: <code>""</code> (Empty string)</p>
|
|
5474
|
-
<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">"icon_180"</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">"NAUTOBOT_BRANDING_FILEPATHS_ICON_180"</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">"icon_192"</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">"NAUTOBOT_BRANDING_FILEPATHS_ICON_192"</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">"icon_mask"</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">"NAUTOBOT_BRANDING_FILEPATHS_ICON_MASK"</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">
|
|
5496
|
+
<a id="__codelineno-2-9" name="__codelineno-2-9" href="#__codelineno-2-9"></a> <span class="s2">"css"</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">"NAUTOBOT_BRANDING_FILEPATHS_CSS"</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">"javascript"</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">"NAUTOBOT_BRANDING_FILEPATHS_JAVASCRIPT"</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
|
|
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
|
|
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">¶</a></h2>
|
|
5506
5520
|
<div class="admonition version-added">
|