Repository: netbox-community/netbox

Issue:
Implement contextual UI documentation
### Environment
* Python version: 3.6.9
* NetBox version: 2.7.7

### Proposed Functionality
Embed documentation for specific objects or concepts directly within the UI. For example, when viewing the "Add device" form, there will be a small "help" button to the side. When clicked, it displays a modal (sort of an in-page pop-up dialog) with the contents of the documentation section relating to the Device model. The content itself would be served via an API endpoint, e.g. `/api/docs/?content=models.dcim.device`.

To avoid redundancy, we can restructure our current documentation to be more modular. For example, the documentation for devices, device types, and related models all exists in the file `docs/core-functionality/devices.md`. We can split out these sections so that each file model gets its own file:

* `docs/models/dcim/devicetype.md`
* `docs/models/dcim/device.md`
* `docs/models/dcim/interface.md`
* etc.

Of course, this would be rather tedious to navigate on [ReadTheDocs](https://netbox.readthedocs.io): Someone who's reading up on device modeling in NetBox would probably prefer to see all related models on one page. We can accomplish this using [`markdown_include`](https://github.com/sethen/markdown-include) plugin for `mkdocs` to pull in content from multiple files into a single easily-browsed page:

```
# Device Modeling in NetBox

Preamble goes here...

{!docs/models/dcim/devicetype.md!}
{!docs/models/dcim/device.md!}
...
```

This approach allows us to serve the core content both on RTD and directly within the UI.

### Use Case
This proposal has several benefits:

1. Provide immediate access to contextual documentation directly within the UI
2. Enable cross-utilization of content through the UI and ReadTheDocs
3. Enforce a more modular, predictable structure for documentation
4. Gaps in the existing documentation will be identified as we deploy links within the UI

### Database Changes
None

### External Dependencies
We'll need [`markdown_include`](https://github.com/sethen/markdown-include) to render the documentation, however we should be able to avoid adding it to NetBox's root `requirements.txt` file. RTD supports a [YAML configuration file](https://docs.readthedocs.io/en/stable/config-file/v2.html) which should allow us to specify a separate requirements file used to build the documentation site.


Discussion:

Resolving patch:
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -75,6 +75,7 @@
 DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
 DEBUG = getattr(configuration, 'DEBUG', False)
 DEVELOPER = getattr(configuration, 'DEVELOPER', False)
+DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
 EMAIL = getattr(configuration, 'EMAIL', {})
 ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
 EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py
--- a/netbox/utilities/forms.py
+++ b/netbox/utilities/forms.py
@@ -498,14 +498,14 @@ def to_python(self, value):
 
 class CommentField(forms.CharField):
     """
-    A textarea with support for GitHub-Flavored Markdown. Exists mostly just to add a standard help_text.
+    A textarea with support for Markdown rendering. Exists mostly just to add a standard help_text.
     """
     widget = forms.Textarea
     default_label = ''
-    # TODO: Port GFM syntax cheat sheet to internal documentation
+    # TODO: Port Markdown cheat sheet to internal documentation
     default_helptext = '<i class="fa fa-info-circle"></i> '\
                        '<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">'\
-                       'GitHub-Flavored Markdown</a> syntax is supported'
+                       'Markdown</a> syntax is supported'
 
     def __init__(self, *args, **kwargs):
         required = kwargs.pop('required', False)
diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py
--- a/netbox/utilities/templatetags/helpers.py
+++ b/netbox/utilities/templatetags/helpers.py
@@ -4,6 +4,7 @@
 
 import yaml
 from django import template
+from django.conf import settings
 from django.urls import NoReverseMatch, reverse
 from django.utils.html import strip_tags
 from django.utils.safestring import mark_safe
@@ -55,16 +56,17 @@ def getkey(value, key):
     return value[key]
 
 
+# TODO: Rename this filter as py-gfm is no longer in use
 @register.filter(is_safe=True)
 def gfm(value):
     """
-    Render text as GitHub-Flavored Markdown
+    Render text as Markdown
     """
     # Strip HTML tags
     value = strip_tags(value)
 
-    # Render Markdown with GFM extension
-    html = markdown(value, extensions=['mdx_gfm'])
+    # Render Markdown
+    html = markdown(value, extensions=['fenced_code'])
 
     return mark_safe(html)
 
@@ -216,6 +218,30 @@ def percentage(x, y):
     return round(x / y * 100)
 
 
+@register.filter()
+def get_docs(model):
+    """
+    Render and return documentation for the specified model.
+    """
+    path = '{}/models/{}/{}.md'.format(
+        settings.DOCS_ROOT,
+        model._meta.app_label,
+        model._meta.model_name
+    )
+    try:
+        with open(path) as docfile:
+            content = docfile.read()
+    except FileNotFoundError:
+        return "Unable to load documentation, file not found: {}".format(path)
+    except IOError:
+        return "Unable to load documentation, error reading file: {}".format(path)
+
+    # Render Markdown with the admonition extension
+    content = markdown(content, extensions=['admonition', 'fenced_code'])
+
+    return mark_safe(content)
+
+
 #
 # Tags
 #


Test patch: