Repository: nautobot/nautobot

Issue:
Custom branding
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for proposing specific new features or enhancements.
    If you have a general idea or question, please start a discussion instead:
    https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot.
-->
### Environment
* Python version: 3.9
* Nautobot version: 1.1.2

<!--
    Describe in detail the new functionality you are proposing. Include any
    specific changes to work flows, data models, or the user interface.
-->
### Proposed Functionality
As Patti the Platform Admin, I want to be able to brand Nautobot with a custom logo and name so that it is more familiar and approachable inside my large organization.

<!--
    Convey an example use case for your proposed feature. Write from the
    perspective of a Nautobot user who would benefit from the proposed
    functionality and describe how.
--->
### Use Case
In a large organization that has entrenched automation efforts, I want to introduce Nautobot as a key component of my strategy, but because of the landscape of the environment, Nautobot must "fit in" with my existing platform which has internal branding and marketing. I want as a minimum, to be able to change the nautobot logo but it would be awesome to be able to replace all references to the name "Nautobot" with my own name. I would be fine with even "<My Name> powered by Nautobot."

This is not about trying to rebrand Nautobot and call it my own, it is more about ensuring Nautobot is well received and accepted by my organization by having it fit into an existing ecosystem.

<!--
    Note any changes to the database schema necessary to support the new
    feature. For example, does the proposal require adding a new model or
    field? (Not all new features require database changes.)
--->
### Database Changes
Likely none, but it may relate to the ability to store certain application settings in the DB.

<!--
    List any new dependencies on external libraries or services that this new
    feature would introduce. For example, does the proposal require the
    installation of a new Python package? (Not all new features introduce new
    dependencies.)
-->
### External Dependencies
N/A, should be solvable internally.


Discussion:
Definitely could see a few use cases for this.  I would add it should be possible from the UI (admin type settings, but not in Django admin as we diminish the use of it).  Ansible AWX has something similar, we can explore what they've done.
But I do agree with "powered by Nautobot" even using the Nautobot logo. This could possibly go in the footer.

>  it would be awesome to be able to replace all references to the name "Nautobot" with my own name

So this would include things like the title of browser tabs and such.
As mentioned above, this may have some interdependency with #370 if configuring this dynamically via the UI is a hard requirement.
While #370 would be cool, it is not a requirement for me here. I was planning on just a setting that points to a file in `MEDIA_ROOT`
@lampwins since you self-assigned this one, we can target this for 1.3, but if you happen to get it done in the next few weeks, we can pull it in 1.2.


Resolving patch:
diff --git a/nautobot/core/api/views.py b/nautobot/core/api/views.py
--- a/nautobot/core/api/views.py
+++ b/nautobot/core/api/views.py
@@ -256,7 +256,7 @@ def perform_destroy(self, instance):
 
 class APIRootView(APIView):
     """
-    This is the root of Nautobot's REST API. API endpoints are arranged by app and model name; e.g. `/api/dcim/sites/`.
+    This is the root of the REST API. API endpoints are arranged by app and model name; e.g. `/api/dcim/sites/`.
     """
 
     _ignore_model_permissions = True
@@ -316,7 +316,7 @@ def get(self, request, format=None):
 
 class StatusView(APIView):
     """
-    A lightweight read-only endpoint for conveying Nautobot's current operational status.
+    A lightweight read-only endpoint for conveying the current operational status.
     """
 
     permission_classes = [IsAuthenticated]
diff --git a/nautobot/core/settings.py b/nautobot/core/settings.py
--- a/nautobot/core/settings.py
+++ b/nautobot/core/settings.py
@@ -532,3 +532,36 @@
 CELERY_RESULT_SERIALIZER = "nautobot_json"
 
 CELERY_BEAT_SCHEDULER = "nautobot.core.celery.schedulers:NautobotDatabaseScheduler"
+
+#
+# Custom branding (logo and title)
+#
+
+# Branding logo locations. The logo takes the place of the Nautobot logo in the top right of the nav bar.
+# The filepath should be relative to the `MEDIA_ROOT`.
+BRANDING_FILEPATHS = {
+    "logo": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_LOGO", None),  # Navbar logo
+    "favicon": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_FAVICON", None),  # Browser favicon
+    "icon_16": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_ICON_16", None),  # 16x16px icon
+    "icon_32": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_ICON_32", None),  # 32x32px icon
+    "icon_180": os.getenv(
+        "NAUTOBOT_BRANDING_FILEPATHS_ICON_180", None
+    ),  # 180x180px icon - used for the apple-touch-icon header
+    "icon_192": os.getenv("NAUTOBOT_BRANDING_FILEPATHS_ICON_192", None),  # 192x192px icon
+    "icon_mask": os.getenv(
+        "NAUTOBOT_BRANDING_FILEPATHS_ICON_MASK", None
+    ),  # mono-chrome icon used for the mask-icon header
+}
+
+# Title to use in place of "Nautobot"
+BRANDING_TITLE = os.getenv("NAUTOBOT_BRANDING_TITLE", "Nautobot")
+
+# Branding URLs (links in the bottom right of the footer)
+BRANDING_URLS = {
+    "code": os.getenv("NAUTOBOT_BRANDING_URLS_CODE", "https://github.com/nautobot/nautobot"),
+    "docs": os.getenv("NAUTOBOT_BRANDING_URLS_DOCS", "https://nautobot.readthedocs.io/"),
+    "help": os.getenv("NAUTOBOT_BRANDING_URLS_HELP", "https://github.com/nautobot/nautobot/wiki"),
+}
+
+# Undocumented link in the bottom right of the footer which is meant to persist any custom branding changes.
+BRANDING_POWERED_BY_URL = "https://nautobot.readthedocs.io/"
diff --git a/nautobot/utilities/templatetags/helpers.py b/nautobot/utilities/templatetags/helpers.py
--- a/nautobot/utilities/templatetags/helpers.py
+++ b/nautobot/utilities/templatetags/helpers.py
@@ -5,6 +5,7 @@
 import yaml
 from django import template
 from django.conf import settings
+from django.templatetags.static import StaticNode
 from django.urls import NoReverseMatch, reverse
 from django.utils.html import strip_tags
 from django.utils.safestring import mark_safe
@@ -380,3 +381,14 @@ def modal_form_as_dialog(form, editing=False, form_name=None, obj=None, obj_type
         "obj": obj,
         "obj_type": obj_type,
     }
+
+
+@register.simple_tag
+def custom_branding_or_static(branding_asset, static_asset):
+    """
+    This tag attempts to return custom branding assets relative to the MEDIA_ROOT and MEDIA_URL, if such
+    branding has been configured in settings, else it returns stock branding via static.
+    """
+    if settings.BRANDING_FILEPATHS.get(branding_asset):
+        return f"{ settings.MEDIA_URL }{ settings.BRANDING_FILEPATHS.get(branding_asset) }"
+    return StaticNode.handle_simple(static_asset)


Test patch: