prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Plugins must define app_name in api/urls.py to be included in API root view
  ### Environment
  * Python version: 3.6.9
  * NetBox version: 2.9.7
  
  ### Steps to Reproduce
  I'm actually not certain whether this is better classified as a bug or a lapse in the documentation, but I'm opting to propose it as a bug because I think it can be addressed by a code change.
  
  1. Create/install a plugin which provides at least one REST API endpoint. (Assume the plugin's name is `myplugin`.)
  2. Within `api/urls.py`, set `app_name = myplugin-api`
  3. Visit `/api/plugins/` in the browser. The plugin should be included.
  4. Remove the `app_name` definition from `api/urls.py`.
  5. Visit `/api/plugins/` in the browser again. The plugin no longer appears.
  
  ### Expected Behavior
  Plugin API endpoints should be detected automatically. AFAICT there's no reason a plugin should need to declare `app_name`. (The core apps do this, but only because they're being included dynamically.)
  
  ### Observed Behavior
  Plugin API endpoints disappear from the list when `app_name` is not declared.
  
  It seems like the need for the `app_name` variable could be obviated within `PluginsAPIRootView._get_plugin_entry()` by changing
  
  ```
  api_app_name = import_object(f"{plugin}.api.urls.app_name")
  ```
  
  to
  
  ```
  api_app_name = f'{app_config.name}-api'
  ```
  
  This actually raises another point: the full URL name for a plugin API endpoint is currently in the format `plugins-api:myplugin-api:url-name`, which seems a bit unwieldy: `plugins-api:myplugin:url-name` should suffice. However, this would be a breaking change for any plugins which rely on reverse URL resolution to their REST API endpoints.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `netbox/extras/plugins/views.py`
  Content:
  ```
  1 from collections import OrderedDict
  2 
  3 from django.apps import apps
  4 from django.conf import settings
  5 from django.shortcuts import render
  6 from django.urls.exceptions import NoReverseMatch
  7 from django.views.generic import View
  8 from rest_framework import permissions
  9 from rest_framework.response import Response
  10 from rest_framework.reverse import reverse
  11 from rest_framework.views import APIView
  12 
  13 from extras.plugins.utils import import_object
  14 
  15 
  16 class InstalledPluginsAdminView(View):
  17     """
  18     Admin view for listing all installed plugins
  19     """
  20     def get(self, request):
  21         plugins = [apps.get_app_config(plugin) for plugin in settings.PLUGINS]
  22         return render(request, 'extras/admin/plugins_list.html', {
  23             'plugins': plugins,
  24         })
  25 
  26 
  27 class InstalledPluginsAPIView(APIView):
  28     """
  29     API view for listing all installed plugins
  30     """
  31     permission_classes = [permissions.IsAdminUser]
  32     _ignore_model_permissions = True
  33     exclude_from_schema = True
  34     swagger_schema = None
  35 
  36     def get_view_name(self):
  37         return "Installed Plugins"
  38 
  39     @staticmethod
  40     def _get_plugin_data(plugin_app_config):
  41         return {
  42             'name': plugin_app_config.verbose_name,
  43             'package': plugin_app_config.name,
  44             'author': plugin_app_config.author,
  45             'author_email': plugin_app_config.author_email,
  46             'description': plugin_app_config.description,
  47             'verison': plugin_app_config.version
  48         }
  49 
  50     def get(self, request, format=None):
  51         return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])
  52 
  53 
  54 class PluginsAPIRootView(APIView):
  55     _ignore_model_permissions = True
  56     exclude_from_schema = True
  57     swagger_schema = None
  58 
  59     def get_view_name(self):
  60         return "Plugins"
  61 
  62     @staticmethod
  63     def _get_plugin_entry(plugin, app_config, request, format):
  64         # Check if the plugin specifies any API URLs
  65         api_app_name = import_object(f"{plugin}.api.urls.app_name")
  66         if api_app_name is None:
  67             # Plugin does not expose an API
  68             return None
  69 
  70         try:
  71             entry = (getattr(app_config, 'base_url', app_config.label), reverse(
  72                 f"plugins-api:{api_app_name}:api-root",
  73                 request=request,
  74                 format=format
  75             ))
  76         except NoReverseMatch:
  77             # The plugin does not include an api-root url
  78             entry = None
  79 
  80         return entry
  81 
  82     def get(self, request, format=None):
  83 
  84         entries = []
  85         for plugin in settings.PLUGINS:
  86             app_config = apps.get_app_config(plugin)
  87             entry = self._get_plugin_entry(plugin, app_config, request, format)
  88             if entry is not None:
  89                 entries.append(entry)
  90 
  91         return Response(OrderedDict((
  92             ('installed-plugins', reverse('plugins-api:plugins-list', request=request, format=format)),
  93             *entries
  94         )))
  95
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/netbox/extras/plugins/views.py b/netbox/extras/plugins/views.py
  --- a/netbox/extras/plugins/views.py
  +++ b/netbox/extras/plugins/views.py
  @@ -10,8 +10,6 @@
   from rest_framework.reverse import reverse
   from rest_framework.views import APIView
   
  -from extras.plugins.utils import import_object
  -
   
   class InstalledPluginsAdminView(View):
       """
  @@ -62,11 +60,7 @@
       @staticmethod
       def _get_plugin_entry(plugin, app_config, request, format):
           # Check if the plugin specifies any API URLs
  -        api_app_name = import_object(f"{plugin}.api.urls.app_name")
  -        if api_app_name is None:
  -            # Plugin does not expose an API
  -            return None
  -
  +        api_app_name = f'{app_config.name}-api'
           try:
               entry = (getattr(app_config, 'base_url', app_config.label), reverse(
                   f"plugins-api:{api_app_name}:api-root",
  
verification_info: {"golden_diff": "diff --git a/netbox/extras/plugins/views.py b/netbox/extras/plugins/views.py\n--- a/netbox/extras/plugins/views.py\n+++ b/netbox/extras/plugins/views.py\n@@ -10,8 +10,6 @@\n from rest_framework.reverse import reverse\n from rest_framework.views import APIView\n \n-from extras.plugins.utils import import_object\n-\n \n class InstalledPluginsAdminView(View):\n     \"\"\"\n@@ -62,11 +60,7 @@\n     @staticmethod\n     def _get_plugin_entry(plugin, app_config, request, format):\n         # Check if the plugin specifies any API URLs\n-        api_app_name = import_object(f\"{plugin}.api.urls.app_name\")\n-        if api_app_name is None:\n-            # Plugin does not expose an API\n-            return None\n-\n+        api_app_name = f'{app_config.name}-api'\n         try:\n             entry = (getattr(app_config, 'base_url', app_config.label), reverse(\n                 f\"plugins-api:{api_app_name}:api-root\",\n", "issue": "Plugins must define app_name in api/urls.py to be included in API root view\n### Environment\r\n* Python version: 3.6.9\r\n* NetBox version: 2.9.7\r\n\r\n### Steps to Reproduce\r\nI'm actually not certain whether this is better classified as a bug or a lapse in the documentation, but I'm opting to propose it as a bug because I think it can be addressed by a code change.\r\n\r\n1. Create/install a plugin which provides at least one REST API endpoint. (Assume the plugin's name is `myplugin`.)\r\n2. Within `api/urls.py`, set `app_name = myplugin-api`\r\n3. Visit `/api/plugins/` in the browser. The plugin should be included.\r\n4. Remove the `app_name` definition from `api/urls.py`.\r\n5. Visit `/api/plugins/` in the browser again. The plugin no longer appears.\r\n\r\n### Expected Behavior\r\nPlugin API endpoints should be detected automatically. AFAICT there's no reason a plugin should need to declare `app_name`. (The core apps do this, but only because they're being included dynamically.)\r\n\r\n### Observed Behavior\r\nPlugin API endpoints disappear from the list when `app_name` is not declared.\r\n\r\nIt seems like the need for the `app_name` variable could be obviated within `PluginsAPIRootView._get_plugin_entry()` by changing\r\n\r\n```\r\napi_app_name = import_object(f\"{plugin}.api.urls.app_name\")\r\n```\r\n\r\nto\r\n\r\n```\r\napi_app_name = f'{app_config.name}-api'\r\n```\r\n\r\nThis actually raises another point: the full URL name for a plugin API endpoint is currently in the format `plugins-api:myplugin-api:url-name`, which seems a bit unwieldy: `plugins-api:myplugin:url-name` should suffice. However, this would be a breaking change for any plugins which rely on reverse URL resolution to their REST API endpoints.\n", "before_files": [{"content": "from collections import OrderedDict\n\nfrom django.apps import apps\nfrom django.conf import settings\nfrom django.shortcuts import render\nfrom django.urls.exceptions import NoReverseMatch\nfrom django.views.generic import View\nfrom rest_framework import permissions\nfrom rest_framework.response import Response\nfrom rest_framework.reverse import reverse\nfrom rest_framework.views import APIView\n\nfrom extras.plugins.utils import import_object\n\n\nclass InstalledPluginsAdminView(View):\n    \"\"\"\n    Admin view for listing all installed plugins\n    \"\"\"\n    def get(self, request):\n        plugins = [apps.get_app_config(plugin) for plugin in settings.PLUGINS]\n        return render(request, 'extras/admin/plugins_list.html', {\n            'plugins': plugins,\n        })\n\n\nclass InstalledPluginsAPIView(APIView):\n    \"\"\"\n    API view for listing all installed plugins\n    \"\"\"\n    permission_classes = [permissions.IsAdminUser]\n    _ignore_model_permissions = True\n    exclude_from_schema = True\n    swagger_schema = None\n\n    def get_view_name(self):\n        return \"Installed Plugins\"\n\n    @staticmethod\n    def _get_plugin_data(plugin_app_config):\n        return {\n            'name': plugin_app_config.verbose_name,\n            'package': plugin_app_config.name,\n            'author': plugin_app_config.author,\n            'author_email': plugin_app_config.author_email,\n            'description': plugin_app_config.description,\n            'verison': plugin_app_config.version\n        }\n\n    def get(self, request, format=None):\n        return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])\n\n\nclass PluginsAPIRootView(APIView):\n    _ignore_model_permissions = True\n    exclude_from_schema = True\n    swagger_schema = None\n\n    def get_view_name(self):\n        return \"Plugins\"\n\n    @staticmethod\n    def _get_plugin_entry(plugin, app_config, request, format):\n        # Check if the plugin specifies any API URLs\n        api_app_name = import_object(f\"{plugin}.api.urls.app_name\")\n        if api_app_name is None:\n            # Plugin does not expose an API\n            return None\n\n        try:\n            entry = (getattr(app_config, 'base_url', app_config.label), reverse(\n                f\"plugins-api:{api_app_name}:api-root\",\n                request=request,\n                format=format\n            ))\n        except NoReverseMatch:\n            # The plugin does not include an api-root url\n            entry = None\n\n        return entry\n\n    def get(self, request, format=None):\n\n        entries = []\n        for plugin in settings.PLUGINS:\n            app_config = apps.get_app_config(plugin)\n            entry = self._get_plugin_entry(plugin, app_config, request, format)\n            if entry is not None:\n                entries.append(entry)\n\n        return Response(OrderedDict((\n            ('installed-plugins', reverse('plugins-api:plugins-list', request=request, format=format)),\n            *entries\n        )))\n", "path": "netbox/extras/plugins/views.py"}], "after_files": [{"content": "from collections import OrderedDict\n\nfrom django.apps import apps\nfrom django.conf import settings\nfrom django.shortcuts import render\nfrom django.urls.exceptions import NoReverseMatch\nfrom django.views.generic import View\nfrom rest_framework import permissions\nfrom rest_framework.response import Response\nfrom rest_framework.reverse import reverse\nfrom rest_framework.views import APIView\n\n\nclass InstalledPluginsAdminView(View):\n    \"\"\"\n    Admin view for listing all installed plugins\n    \"\"\"\n    def get(self, request):\n        plugins = [apps.get_app_config(plugin) for plugin in settings.PLUGINS]\n        return render(request, 'extras/admin/plugins_list.html', {\n            'plugins': plugins,\n        })\n\n\nclass InstalledPluginsAPIView(APIView):\n    \"\"\"\n    API view for listing all installed plugins\n    \"\"\"\n    permission_classes = [permissions.IsAdminUser]\n    _ignore_model_permissions = True\n    exclude_from_schema = True\n    swagger_schema = None\n\n    def get_view_name(self):\n        return \"Installed Plugins\"\n\n    @staticmethod\n    def _get_plugin_data(plugin_app_config):\n        return {\n            'name': plugin_app_config.verbose_name,\n            'package': plugin_app_config.name,\n            'author': plugin_app_config.author,\n            'author_email': plugin_app_config.author_email,\n            'description': plugin_app_config.description,\n            'verison': plugin_app_config.version\n        }\n\n    def get(self, request, format=None):\n        return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])\n\n\nclass PluginsAPIRootView(APIView):\n    _ignore_model_permissions = True\n    exclude_from_schema = True\n    swagger_schema = None\n\n    def get_view_name(self):\n        return \"Plugins\"\n\n    @staticmethod\n    def _get_plugin_entry(plugin, app_config, request, format):\n        # Check if the plugin specifies any API URLs\n        api_app_name = f'{app_config.name}-api'\n        try:\n            entry = (getattr(app_config, 'base_url', app_config.label), reverse(\n                f\"plugins-api:{api_app_name}:api-root\",\n                request=request,\n                format=format\n            ))\n        except NoReverseMatch:\n            # The plugin does not include an api-root url\n            entry = None\n\n        return entry\n\n    def get(self, request, format=None):\n\n        entries = []\n        for plugin in settings.PLUGINS:\n            app_config = apps.get_app_config(plugin)\n            entry = self._get_plugin_entry(plugin, app_config, request, format)\n            if entry is not None:\n                entries.append(entry)\n\n        return Response(OrderedDict((\n            ('installed-plugins', reverse('plugins-api:plugins-list', request=request, format=format)),\n            *entries\n        )))\n", "path": "netbox/extras/plugins/views.py"}]}