We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Plugin with git, but no commits causes all plugins to fail to load
### Please verify that this bug has NOT been raised before.

- [X] I checked and didn't find a similar issue

### Describe the bug*

Creating a plugin in `InvenTree/plugins/your_plugin/` with the proper files required, and then initiating a git repo in this directory will cause all plugins to fail on the next reload of the server

### Steps to Reproduce

1. `mkdir InvenTree/plugins/someplugin && touch InvenTree/plugins/someplugin/__init__.py && cd InvenTree/plugins/someplugin && git init`
2. 2. Start up the InvenTree instance
3. All plugins will be listed with "?" icons, and even installed plugins will be unavailable

### Expected behaviour

Ignore that no commits have been issues

### Deployment Method

- [X] Docker
- [X] Bare metal

### Version Information

# Version Information:
InvenTree-Version: 0.14.0 dev
Django Version: 3.2.23
Commit Hash: 5d018e8
Commit Date: 2024-01-15
Commit Branch: details-panel
Database: sqlite3
Debug-Mode: True
Deployed using Docker: True
Platform: Linux-5.15.133.1-microsoft-standard-WSL2-x86_64-with
Installer: DOC

Active plugins: []


### Please verify if you can reproduce this bug on the demo site.

- [ ] I can reproduce this bug on the demo site.

### Relevant log output

```shell
a
```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `InvenTree/plugin/helpers.py`
Content:
```
1 """Helpers for plugin app."""
2 
3 import inspect
4 import logging
5 import os
6 import pathlib
7 import pkgutil
8 import sysconfig
9 import traceback
10 from importlib.metadata import entry_points
11 
12 from django import template
13 from django.conf import settings
14 from django.core.exceptions import AppRegistryNotReady
15 from django.db.utils import IntegrityError
16 
17 logger = logging.getLogger('inventree')
18 
19 
20 # region logging / errors
21 class IntegrationPluginError(Exception):
22     """Error that encapsulates another error and adds the path / reference of the raising plugin."""
23 
24     def __init__(self, path, message):
25         """Init a plugin error.
26 
27         Args:
28             path: Path on which the error occurred - used to find out which plugin it was
29             message: The original error message
30         """
31         self.path = path
32         self.message = message
33 
34     def __str__(self):
35         """Returns the error message."""
36         return self.message  # pragma: no cover
37 
38 
39 class MixinImplementationError(ValueError):
40     """Error if mixin was implemented wrong in plugin.
41 
42     Mostly raised if constant is missing
43     """
44 
45     pass
46 
47 
48 class MixinNotImplementedError(NotImplementedError):
49     """Error if necessary mixin function was not overwritten."""
50 
51     pass
52 
53 
54 def log_error(error, reference: str = 'general'):
55     """Log an plugin error."""
56     from plugin import registry
57 
58     # make sure the registry is set up
59     if reference not in registry.errors:
60         registry.errors[reference] = []
61 
62     # add error to stack
63     registry.errors[reference].append(error)
64 
65 
66 def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: str = ''):
67     """Handles an error and casts it as an IntegrationPluginError."""
68     package_path = traceback.extract_tb(error.__traceback__)[-1].filename
69     install_path = sysconfig.get_paths()['purelib']
70 
71     try:
72         package_name = pathlib.Path(package_path).relative_to(install_path).parts[0]
73     except ValueError:
74         # is file - loaded -> form a name for that
75         try:
76             path_obj = pathlib.Path(package_path).relative_to(settings.BASE_DIR)
77             path_parts = [*path_obj.parts]
78             path_parts[-1] = path_parts[-1].replace(
79                 path_obj.suffix, ''
80             )  # remove suffix
81 
82             # remove path prefixes
83             if path_parts[0] == 'plugin':
84                 path_parts.remove('plugin')
85                 path_parts.pop(0)
86             else:
87                 path_parts.remove('plugins')  # pragma: no cover
88 
89             package_name = '.'.join(path_parts)
90         except Exception:
91             package_name = package_path
92 
93     if do_log:
94         log_kwargs = {}
95         if log_name:
96             log_kwargs['reference'] = log_name
97         log_error({package_name: str(error)}, **log_kwargs)
98 
99     if do_raise:
100         # do a straight raise if we are playing with environment variables at execution time, ignore the broken sample
101         if (
102             settings.TESTING_ENV
103             and package_name != 'integration.broken_sample'
104             and isinstance(error, IntegrityError)
105         ):
106             raise error  # pragma: no cover
107 
108         raise IntegrationPluginError(package_name, str(error))
109 
110 
111 def get_entrypoints():
112     """Returns list for entrypoints for InvenTree plugins."""
113     return entry_points().get('inventree_plugins', [])
114 
115 
116 # endregion
117 
118 
119 # region git-helpers
120 def get_git_log(path):
121     """Get dict with info of the last commit to file named in path."""
122     import datetime
123 
124     from dulwich.repo import NotGitRepository, Repo
125 
126     from InvenTree.ready import isInTestMode
127 
128     output = None
129     path = os.path.abspath(path)
130 
131     if os.path.exists(path) and os.path.isfile(path):
132         path = os.path.dirname(path)
133 
134     # only do this if we are not in test mode
135     if not isInTestMode():  # pragma: no cover
136         try:
137             repo = Repo(path)
138             head = repo.head()
139             commit = repo[head]
140 
141             output = [
142                 head.decode(),
143                 commit.author.decode().split('<')[0][:-1],
144                 commit.author.decode().split('<')[1][:-1],
145                 datetime.datetime.fromtimestamp(commit.author_time).isoformat(),
146                 commit.message.decode().split('\n')[0],
147             ]
148         except NotGitRepository:
149             pass
150 
151     if not output:
152         output = 5 * ['']  # pragma: no cover
153 
154     return {
155         'hash': output[0],
156         'author': output[1],
157         'mail': output[2],
158         'date': output[3],
159         'message': output[4],
160     }
161 
162 
163 # endregion
164 
165 
166 # region plugin finders
167 def get_modules(pkg, path=None):
168     """Get all modules in a package."""
169     context = {}
170 
171     if path is None:
172         path = pkg.__path__
173     elif type(path) is not list:
174         path = [path]
175 
176     for loader, name, _ in pkgutil.walk_packages(path):
177         try:
178             module = loader.find_module(name).load_module(name)
179             pkg_names = getattr(module, '__all__', None)
180             for k, v in vars(module).items():
181                 if not k.startswith('_') and (pkg_names is None or k in pkg_names):
182                     context[k] = v
183             context[name] = module
184         except AppRegistryNotReady:  # pragma: no cover
185             pass
186         except Exception as error:
187             # this 'protects' against malformed plugin modules by more or less silently failing
188 
189             # log to stack
190             log_error({name: str(error)}, 'discovery')
191 
192     return [v for k, v in context.items()]
193 
194 
195 def get_classes(module):
196     """Get all classes in a given module."""
197     return inspect.getmembers(module, inspect.isclass)
198 
199 
200 def get_plugins(pkg, baseclass, path=None):
201     """Return a list of all modules under a given package.
202 
203     - Modules must be a subclass of the provided 'baseclass'
204     - Modules must have a non-empty NAME parameter
205     """
206     plugins = []
207 
208     modules = get_modules(pkg, path=path)
209 
210     # Iterate through each module in the package
211     for mod in modules:
212         # Iterate through each class in the module
213         for item in get_classes(mod):
214             plugin = item[1]
215             if issubclass(plugin, baseclass) and plugin.NAME:
216                 plugins.append(plugin)
217 
218     return plugins
219 
220 
221 # endregion
222 
223 
224 # region templates
225 def render_template(plugin, template_file, context=None):
226     """Locate and render a template file, available in the global template context."""
227     try:
228         tmp = template.loader.get_template(template_file)
229     except template.TemplateDoesNotExist:
230         logger.exception(
231             "Plugin %s could not locate template '%s'", plugin.slug, template_file
232         )
233 
234         return f"""
235         <div class='alert alert-block alert-danger'>
236         Template file <em>{template_file}</em> does not exist.
237         </div>
238         """
239 
240     # Render with the provided context
241     html = tmp.render(context)
242 
243     return html
244 
245 
246 def render_text(text, context=None):
247     """Locate a raw string with provided context."""
248     ctx = template.Context(context)
249 
250     return template.Template(text).render(ctx)
251 
252 
253 # endregion
254
```
--- 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()

```

diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py
--- a/InvenTree/plugin/helpers.py
+++ b/InvenTree/plugin/helpers.py
@@ -145,6 +145,8 @@
                 datetime.datetime.fromtimestamp(commit.author_time).isoformat(),
                 commit.message.decode().split('\n')[0],
             ]
+        except KeyError as err:
+            logger.debug('No HEAD tag found in git repo at path %s', path)
         except NotGitRepository:
             pass

{"golden_diff": "diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py\n--- a/InvenTree/plugin/helpers.py\n+++ b/InvenTree/plugin/helpers.py\n@@ -145,6 +145,8 @@\n                 datetime.datetime.fromtimestamp(commit.author_time).isoformat(),\n                 commit.message.decode().split('\\n')[0],\n             ]\n+        except KeyError as err:\n+            logger.debug('No HEAD tag found in git repo at path %s', path)\n         except NotGitRepository:\n             pass\n", "issue": "Plugin with git, but no commits causes all plugins to fail to load\n### Please verify that this bug has NOT been raised before.\n\n- [X] I checked and didn't find a similar issue\n\n### Describe the bug*\n\nCreating a plugin in `InvenTree/plugins/your_plugin/` with the proper files required, and then initiating a git repo in this directory will cause all plugins to fail on the next reload of the server\n\n### Steps to Reproduce\n\n1. `mkdir InvenTree/plugins/someplugin && touch InvenTree/plugins/someplugin/__init__.py && cd InvenTree/plugins/someplugin && git init`\r\n2. 2. Start up the InvenTree instance\r\n3. All plugins will be listed with \"?\" icons, and even installed plugins will be unavailable\n\n### Expected behaviour\n\nIgnore that no commits have been issues\n\n### Deployment Method\n\n- [X] Docker\n- [X] Bare metal\n\n### Version Information\n\n# Version Information:\r\nInvenTree-Version: 0.14.0 dev\r\nDjango Version: 3.2.23\r\nCommit Hash: 5d018e8\r\nCommit Date: 2024-01-15\r\nCommit Branch: details-panel\r\nDatabase: sqlite3\r\nDebug-Mode: True\r\nDeployed using Docker: True\r\nPlatform: Linux-5.15.133.1-microsoft-standard-WSL2-x86_64-with\r\nInstaller: DOC\r\n\r\nActive plugins: []\r\n\n\n### Please verify if you can reproduce this bug on the demo site.\n\n- [ ] I can reproduce this bug on the demo site.\n\n### Relevant log output\n\n```shell\na\n```\n\n", "before_files": [{"content": "\"\"\"Helpers for plugin app.\"\"\"\n\nimport inspect\nimport logging\nimport os\nimport pathlib\nimport pkgutil\nimport sysconfig\nimport traceback\nfrom importlib.metadata import entry_points\n\nfrom django import template\nfrom django.conf import settings\nfrom django.core.exceptions import AppRegistryNotReady\nfrom django.db.utils import IntegrityError\n\nlogger = logging.getLogger('inventree')\n\n\n# region logging / errors\nclass IntegrationPluginError(Exception):\n    \"\"\"Error that encapsulates another error and adds the path / reference of the raising plugin.\"\"\"\n\n    def __init__(self, path, message):\n        \"\"\"Init a plugin error.\n\n        Args:\n            path: Path on which the error occurred - used to find out which plugin it was\n            message: The original error message\n        \"\"\"\n        self.path = path\n        self.message = message\n\n    def __str__(self):\n        \"\"\"Returns the error message.\"\"\"\n        return self.message  # pragma: no cover\n\n\nclass MixinImplementationError(ValueError):\n    \"\"\"Error if mixin was implemented wrong in plugin.\n\n    Mostly raised if constant is missing\n    \"\"\"\n\n    pass\n\n\nclass MixinNotImplementedError(NotImplementedError):\n    \"\"\"Error if necessary mixin function was not overwritten.\"\"\"\n\n    pass\n\n\ndef log_error(error, reference: str = 'general'):\n    \"\"\"Log an plugin error.\"\"\"\n    from plugin import registry\n\n    # make sure the registry is set up\n    if reference not in registry.errors:\n        registry.errors[reference] = []\n\n    # add error to stack\n    registry.errors[reference].append(error)\n\n\ndef handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: str = ''):\n    \"\"\"Handles an error and casts it as an IntegrationPluginError.\"\"\"\n    package_path = traceback.extract_tb(error.__traceback__)[-1].filename\n    install_path = sysconfig.get_paths()['purelib']\n\n    try:\n        package_name = pathlib.Path(package_path).relative_to(install_path).parts[0]\n    except ValueError:\n        # is file - loaded -> form a name for that\n        try:\n            path_obj = pathlib.Path(package_path).relative_to(settings.BASE_DIR)\n            path_parts = [*path_obj.parts]\n            path_parts[-1] = path_parts[-1].replace(\n                path_obj.suffix, ''\n            )  # remove suffix\n\n            # remove path prefixes\n            if path_parts[0] == 'plugin':\n                path_parts.remove('plugin')\n                path_parts.pop(0)\n            else:\n                path_parts.remove('plugins')  # pragma: no cover\n\n            package_name = '.'.join(path_parts)\n        except Exception:\n            package_name = package_path\n\n    if do_log:\n        log_kwargs = {}\n        if log_name:\n            log_kwargs['reference'] = log_name\n        log_error({package_name: str(error)}, **log_kwargs)\n\n    if do_raise:\n        # do a straight raise if we are playing with environment variables at execution time, ignore the broken sample\n        if (\n            settings.TESTING_ENV\n            and package_name != 'integration.broken_sample'\n            and isinstance(error, IntegrityError)\n        ):\n            raise error  # pragma: no cover\n\n        raise IntegrationPluginError(package_name, str(error))\n\n\ndef get_entrypoints():\n    \"\"\"Returns list for entrypoints for InvenTree plugins.\"\"\"\n    return entry_points().get('inventree_plugins', [])\n\n\n# endregion\n\n\n# region git-helpers\ndef get_git_log(path):\n    \"\"\"Get dict with info of the last commit to file named in path.\"\"\"\n    import datetime\n\n    from dulwich.repo import NotGitRepository, Repo\n\n    from InvenTree.ready import isInTestMode\n\n    output = None\n    path = os.path.abspath(path)\n\n    if os.path.exists(path) and os.path.isfile(path):\n        path = os.path.dirname(path)\n\n    # only do this if we are not in test mode\n    if not isInTestMode():  # pragma: no cover\n        try:\n            repo = Repo(path)\n            head = repo.head()\n            commit = repo[head]\n\n            output = [\n                head.decode(),\n                commit.author.decode().split('<')[0][:-1],\n                commit.author.decode().split('<')[1][:-1],\n                datetime.datetime.fromtimestamp(commit.author_time).isoformat(),\n                commit.message.decode().split('\\n')[0],\n            ]\n        except NotGitRepository:\n            pass\n\n    if not output:\n        output = 5 * ['']  # pragma: no cover\n\n    return {\n        'hash': output[0],\n        'author': output[1],\n        'mail': output[2],\n        'date': output[3],\n        'message': output[4],\n    }\n\n\n# endregion\n\n\n# region plugin finders\ndef get_modules(pkg, path=None):\n    \"\"\"Get all modules in a package.\"\"\"\n    context = {}\n\n    if path is None:\n        path = pkg.__path__\n    elif type(path) is not list:\n        path = [path]\n\n    for loader, name, _ in pkgutil.walk_packages(path):\n        try:\n            module = loader.find_module(name).load_module(name)\n            pkg_names = getattr(module, '__all__', None)\n            for k, v in vars(module).items():\n                if not k.startswith('_') and (pkg_names is None or k in pkg_names):\n                    context[k] = v\n            context[name] = module\n        except AppRegistryNotReady:  # pragma: no cover\n            pass\n        except Exception as error:\n            # this 'protects' against malformed plugin modules by more or less silently failing\n\n            # log to stack\n            log_error({name: str(error)}, 'discovery')\n\n    return [v for k, v in context.items()]\n\n\ndef get_classes(module):\n    \"\"\"Get all classes in a given module.\"\"\"\n    return inspect.getmembers(module, inspect.isclass)\n\n\ndef get_plugins(pkg, baseclass, path=None):\n    \"\"\"Return a list of all modules under a given package.\n\n    - Modules must be a subclass of the provided 'baseclass'\n    - Modules must have a non-empty NAME parameter\n    \"\"\"\n    plugins = []\n\n    modules = get_modules(pkg, path=path)\n\n    # Iterate through each module in the package\n    for mod in modules:\n        # Iterate through each class in the module\n        for item in get_classes(mod):\n            plugin = item[1]\n            if issubclass(plugin, baseclass) and plugin.NAME:\n                plugins.append(plugin)\n\n    return plugins\n\n\n# endregion\n\n\n# region templates\ndef render_template(plugin, template_file, context=None):\n    \"\"\"Locate and render a template file, available in the global template context.\"\"\"\n    try:\n        tmp = template.loader.get_template(template_file)\n    except template.TemplateDoesNotExist:\n        logger.exception(\n            \"Plugin %s could not locate template '%s'\", plugin.slug, template_file\n        )\n\n        return f\"\"\"\n        <div class='alert alert-block alert-danger'>\n        Template file <em>{template_file}</em> does not exist.\n        </div>\n        \"\"\"\n\n    # Render with the provided context\n    html = tmp.render(context)\n\n    return html\n\n\ndef render_text(text, context=None):\n    \"\"\"Locate a raw string with provided context.\"\"\"\n    ctx = template.Context(context)\n\n    return template.Template(text).render(ctx)\n\n\n# endregion\n", "path": "InvenTree/plugin/helpers.py"}], "after_files": [{"content": "\"\"\"Helpers for plugin app.\"\"\"\n\nimport inspect\nimport logging\nimport os\nimport pathlib\nimport pkgutil\nimport sysconfig\nimport traceback\nfrom importlib.metadata import entry_points\n\nfrom django import template\nfrom django.conf import settings\nfrom django.core.exceptions import AppRegistryNotReady\nfrom django.db.utils import IntegrityError\n\nlogger = logging.getLogger('inventree')\n\n\n# region logging / errors\nclass IntegrationPluginError(Exception):\n    \"\"\"Error that encapsulates another error and adds the path / reference of the raising plugin.\"\"\"\n\n    def __init__(self, path, message):\n        \"\"\"Init a plugin error.\n\n        Args:\n            path: Path on which the error occurred - used to find out which plugin it was\n            message: The original error message\n        \"\"\"\n        self.path = path\n        self.message = message\n\n    def __str__(self):\n        \"\"\"Returns the error message.\"\"\"\n        return self.message  # pragma: no cover\n\n\nclass MixinImplementationError(ValueError):\n    \"\"\"Error if mixin was implemented wrong in plugin.\n\n    Mostly raised if constant is missing\n    \"\"\"\n\n    pass\n\n\nclass MixinNotImplementedError(NotImplementedError):\n    \"\"\"Error if necessary mixin function was not overwritten.\"\"\"\n\n    pass\n\n\ndef log_error(error, reference: str = 'general'):\n    \"\"\"Log an plugin error.\"\"\"\n    from plugin import registry\n\n    # make sure the registry is set up\n    if reference not in registry.errors:\n        registry.errors[reference] = []\n\n    # add error to stack\n    registry.errors[reference].append(error)\n\n\ndef handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: str = ''):\n    \"\"\"Handles an error and casts it as an IntegrationPluginError.\"\"\"\n    package_path = traceback.extract_tb(error.__traceback__)[-1].filename\n    install_path = sysconfig.get_paths()['purelib']\n\n    try:\n        package_name = pathlib.Path(package_path).relative_to(install_path).parts[0]\n    except ValueError:\n        # is file - loaded -> form a name for that\n        try:\n            path_obj = pathlib.Path(package_path).relative_to(settings.BASE_DIR)\n            path_parts = [*path_obj.parts]\n            path_parts[-1] = path_parts[-1].replace(\n                path_obj.suffix, ''\n            )  # remove suffix\n\n            # remove path prefixes\n            if path_parts[0] == 'plugin':\n                path_parts.remove('plugin')\n                path_parts.pop(0)\n            else:\n                path_parts.remove('plugins')  # pragma: no cover\n\n            package_name = '.'.join(path_parts)\n        except Exception:\n            package_name = package_path\n\n    if do_log:\n        log_kwargs = {}\n        if log_name:\n            log_kwargs['reference'] = log_name\n        log_error({package_name: str(error)}, **log_kwargs)\n\n    if do_raise:\n        # do a straight raise if we are playing with environment variables at execution time, ignore the broken sample\n        if (\n            settings.TESTING_ENV\n            and package_name != 'integration.broken_sample'\n            and isinstance(error, IntegrityError)\n        ):\n            raise error  # pragma: no cover\n\n        raise IntegrationPluginError(package_name, str(error))\n\n\ndef get_entrypoints():\n    \"\"\"Returns list for entrypoints for InvenTree plugins.\"\"\"\n    return entry_points().get('inventree_plugins', [])\n\n\n# endregion\n\n\n# region git-helpers\ndef get_git_log(path):\n    \"\"\"Get dict with info of the last commit to file named in path.\"\"\"\n    import datetime\n\n    from dulwich.repo import NotGitRepository, Repo\n\n    from InvenTree.ready import isInTestMode\n\n    output = None\n    path = os.path.abspath(path)\n\n    if os.path.exists(path) and os.path.isfile(path):\n        path = os.path.dirname(path)\n\n    # only do this if we are not in test mode\n    if not isInTestMode():  # pragma: no cover\n        try:\n            repo = Repo(path)\n            head = repo.head()\n            commit = repo[head]\n\n            output = [\n                head.decode(),\n                commit.author.decode().split('<')[0][:-1],\n                commit.author.decode().split('<')[1][:-1],\n                datetime.datetime.fromtimestamp(commit.author_time).isoformat(),\n                commit.message.decode().split('\\n')[0],\n            ]\n        except KeyError as err:\n            logger.debug('No HEAD tag found in git repo at path %s', path)\n        except NotGitRepository:\n            pass\n\n    if not output:\n        output = 5 * ['']  # pragma: no cover\n\n    return {\n        'hash': output[0],\n        'author': output[1],\n        'mail': output[2],\n        'date': output[3],\n        'message': output[4],\n    }\n\n\n# endregion\n\n\n# region plugin finders\ndef get_modules(pkg, path=None):\n    \"\"\"Get all modules in a package.\"\"\"\n    context = {}\n\n    if path is None:\n        path = pkg.__path__\n    elif type(path) is not list:\n        path = [path]\n\n    for loader, name, _ in pkgutil.walk_packages(path):\n        try:\n            module = loader.find_module(name).load_module(name)\n            pkg_names = getattr(module, '__all__', None)\n            for k, v in vars(module).items():\n                if not k.startswith('_') and (pkg_names is None or k in pkg_names):\n                    context[k] = v\n            context[name] = module\n        except AppRegistryNotReady:  # pragma: no cover\n            pass\n        except Exception as error:\n            # this 'protects' against malformed plugin modules by more or less silently failing\n\n            # log to stack\n            log_error({name: str(error)}, 'discovery')\n\n    return [v for k, v in context.items()]\n\n\ndef get_classes(module):\n    \"\"\"Get all classes in a given module.\"\"\"\n    return inspect.getmembers(module, inspect.isclass)\n\n\ndef get_plugins(pkg, baseclass, path=None):\n    \"\"\"Return a list of all modules under a given package.\n\n    - Modules must be a subclass of the provided 'baseclass'\n    - Modules must have a non-empty NAME parameter\n    \"\"\"\n    plugins = []\n\n    modules = get_modules(pkg, path=path)\n\n    # Iterate through each module in the package\n    for mod in modules:\n        # Iterate through each class in the module\n        for item in get_classes(mod):\n            plugin = item[1]\n            if issubclass(plugin, baseclass) and plugin.NAME:\n                plugins.append(plugin)\n\n    return plugins\n\n\n# endregion\n\n\n# region templates\ndef render_template(plugin, template_file, context=None):\n    \"\"\"Locate and render a template file, available in the global template context.\"\"\"\n    try:\n        tmp = template.loader.get_template(template_file)\n    except template.TemplateDoesNotExist:\n        logger.exception(\n            \"Plugin %s could not locate template '%s'\", plugin.slug, template_file\n        )\n\n        return f\"\"\"\n        <div class='alert alert-block alert-danger'>\n        Template file <em>{template_file}</em> does not exist.\n        </div>\n        \"\"\"\n\n    # Render with the provided context\n    html = tmp.render(context)\n\n    return html\n\n\ndef render_text(text, context=None):\n    \"\"\"Locate a raw string with provided context.\"\"\"\n    ctx = template.Context(context)\n\n    return template.Template(text).render(ctx)\n\n\n# endregion\n", "path": "InvenTree/plugin/helpers.py"}]}