mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1304 from SchrodingersGat/email-support
Support for email settings
This commit is contained in:
commit
79dc66e840
@ -32,6 +32,7 @@ def health_status(request):
|
||||
|
||||
status = {
|
||||
'django_q_running': InvenTree.status.is_worker_running(),
|
||||
'email_configured': InvenTree.status.is_email_configured(),
|
||||
}
|
||||
|
||||
all_healthy = True
|
||||
|
@ -52,6 +52,10 @@ class AuthRequiredMiddleware(object):
|
||||
if request.path_info.startswith('/static/'):
|
||||
authorized = True
|
||||
|
||||
# Unauthorized users can access the login page
|
||||
elif request.path_info.startswith('/accounts/'):
|
||||
authorized = True
|
||||
|
||||
elif 'Authorization' in request.headers.keys():
|
||||
auth = request.headers['Authorization'].strip()
|
||||
|
||||
|
@ -495,6 +495,51 @@ CURRENCIES = CONFIG.get(
|
||||
# TODO - Allow live web-based backends in the future
|
||||
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeManualExchangeBackend'
|
||||
|
||||
# Extract email settings from the config file
|
||||
email_config = CONFIG.get('email', {})
|
||||
|
||||
EMAIL_BACKEND = get_setting(
|
||||
'django.core.mail.backends.smtp.EmailBackend',
|
||||
email_config.get('backend', '')
|
||||
)
|
||||
|
||||
# Email backend settings
|
||||
EMAIL_HOST = get_setting(
|
||||
'INVENTREE_EMAIL_HOST',
|
||||
email_config.get('host', '')
|
||||
)
|
||||
|
||||
EMAIL_PORT = get_setting(
|
||||
'INVENTREE_EMAIL_PORT',
|
||||
email_config.get('port', 25)
|
||||
)
|
||||
|
||||
EMAIL_HOST_USER = get_setting(
|
||||
'INVENTREE_EMAIL_USERNAME',
|
||||
email_config.get('username', ''),
|
||||
)
|
||||
|
||||
EMAIL_HOST_PASSWORD = get_setting(
|
||||
'INVENTREE_EMAIL_PASSWORD',
|
||||
email_config.get('password', ''),
|
||||
)
|
||||
|
||||
EMAIL_SUBJECT_PREFIX = '[InvenTree] '
|
||||
|
||||
EMAIL_USE_LOCALTIME = False
|
||||
|
||||
EMAIL_USE_TLS = get_setting(
|
||||
'INVENTREE_EMAIL_TLS',
|
||||
email_config.get('tls', False),
|
||||
)
|
||||
|
||||
EMAIL_USE_SSL = get_setting(
|
||||
'INVENTREE_EMAIL_SSL',
|
||||
email_config.get('ssl', False),
|
||||
)
|
||||
|
||||
EMAIL_TIMEOUT = 60
|
||||
|
||||
LOCALE_PATHS = (
|
||||
os.path.join(BASE_DIR, 'locale/'),
|
||||
)
|
||||
|
@ -11,6 +11,9 @@ from datetime import datetime, timedelta
|
||||
from django_q.models import Success
|
||||
from django_q.monitor import Stat
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
@ -43,6 +46,30 @@ def is_worker_running(**kwargs):
|
||||
return results.exists()
|
||||
|
||||
|
||||
def is_email_configured():
|
||||
"""
|
||||
Check if email backend is configured.
|
||||
|
||||
NOTE: This does not check if the configuration is valid!
|
||||
"""
|
||||
|
||||
configured = True
|
||||
|
||||
if not settings.EMAIL_HOST:
|
||||
logger.warning("EMAIL_HOST is not configured")
|
||||
configured = False
|
||||
|
||||
if not settings.EMAIL_HOST_USER:
|
||||
logger.warning("EMAIL_HOST_USER is not configured")
|
||||
configured = False
|
||||
|
||||
if not settings.EMAIL_HOST_PASSWORD:
|
||||
logger.warning("EMAIL_HOST_PASSWORD is not configured")
|
||||
configured = False
|
||||
|
||||
return configured
|
||||
|
||||
|
||||
def check_system_health(**kwargs):
|
||||
"""
|
||||
Check that the InvenTree system is running OK.
|
||||
@ -56,6 +83,10 @@ def check_system_health(**kwargs):
|
||||
result = False
|
||||
logger.warning(_("Background worker check failed"))
|
||||
|
||||
if not is_email_configured():
|
||||
result = False
|
||||
logger.warning(_("Email backend not configured"))
|
||||
|
||||
if not result:
|
||||
logger.warning(_("InvenTree system health checks failed"))
|
||||
|
||||
|
@ -51,6 +51,24 @@ def schedule_task(taskname, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def offload_task(taskname, *args, **kwargs):
|
||||
"""
|
||||
Create an AsyncTask.
|
||||
This is different to a 'scheduled' task,
|
||||
in that it only runs once!
|
||||
"""
|
||||
|
||||
try:
|
||||
from django_q.tasks import AsyncTask
|
||||
except (AppRegistryNotReady):
|
||||
logger.warning("Could not offload task - app registry not ready")
|
||||
return
|
||||
|
||||
task = AsyncTask(taskname, *args, **kwargs)
|
||||
|
||||
task.run()
|
||||
|
||||
|
||||
def heartbeat():
|
||||
"""
|
||||
Simple task which runs at 5 minute intervals,
|
||||
@ -141,3 +159,20 @@ def check_for_updates():
|
||||
tag,
|
||||
None
|
||||
)
|
||||
|
||||
|
||||
def send_email(subject, body, recipients, from_email=None):
|
||||
"""
|
||||
Send an email with the specified subject and body,
|
||||
to the specified recipients list.
|
||||
"""
|
||||
|
||||
if type(recipients) == str:
|
||||
recipients = [recipients]
|
||||
|
||||
offload_task(
|
||||
'django.core.mail.send_mail',
|
||||
subject, body,
|
||||
from_email,
|
||||
recipients,
|
||||
)
|
||||
|
@ -133,7 +133,7 @@ urlpatterns = [
|
||||
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
|
||||
url(r'^login/?', auth_views.LoginView.as_view(), name='login'),
|
||||
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html'), name='logout'),
|
||||
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logged_out.html'), name='logout'),
|
||||
|
||||
url(r'^settings/', include(settings_urls)),
|
||||
|
||||
@ -143,6 +143,7 @@ urlpatterns = [
|
||||
url(r'^admin/error_log/', include('error_report.urls')),
|
||||
url(r'^admin/shell/', include('django_admin_shell.urls')),
|
||||
url(r'^admin/', admin.site.urls, name='inventree-admin'),
|
||||
url(r'accounts/', include('django.contrib.auth.urls')),
|
||||
|
||||
url(r'^index/', IndexView.as_view(), name='index'),
|
||||
url(r'^search/', SearchView.as_view(), name='search'),
|
||||
|
@ -63,6 +63,31 @@ currencies:
|
||||
- NZD
|
||||
- USD
|
||||
|
||||
# Email backend configuration
|
||||
# Ref: https://docs.djangoproject.com/en/dev/topics/email/
|
||||
# Available options:
|
||||
# host: Email server host address
|
||||
# port: Email port
|
||||
# username: Account username
|
||||
# password: Account password
|
||||
# prefix: Email subject prefix
|
||||
# tls: Enable TLS support
|
||||
# ssl: Enable SSL support
|
||||
|
||||
# Alternatively, these options can all be set using environment variables,
|
||||
# with the INVENTREE_EMAIL_ prefix:
|
||||
# e.g. INVENTREE_EMAIL_HOST / INVENTREE_EMAIL_PORT / INVENTREE_EMAIL_USERNAME
|
||||
# Refer to the InvenTree documentation for more information
|
||||
|
||||
email:
|
||||
# backend: 'django.core.mail.backends.smtp.EmailBackend'
|
||||
host: ''
|
||||
port: 25
|
||||
username: ''
|
||||
password: ''
|
||||
tls: False
|
||||
ssl: False
|
||||
|
||||
# Set debug to False to run in production mode
|
||||
# Use the environment variable INVENTREE_DEBUG
|
||||
debug: True
|
||||
|
59
InvenTree/templates/registration/logged_out.html
Normal file
59
InvenTree/templates/registration/logged_out.html
Normal file
@ -0,0 +1,59 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-table.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
|
||||
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
|
||||
|
||||
<style>
|
||||
.login-error {
|
||||
color: #F88;
|
||||
}
|
||||
</style>
|
||||
|
||||
<title>
|
||||
InvenTree
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body class='login-screen'>
|
||||
|
||||
<div class='main body-wrapper login-screen'>
|
||||
|
||||
<div class='login-container'>
|
||||
<div class="row">
|
||||
<div class='container-fluid'>
|
||||
<div class='clearfix content-heading login-header'>
|
||||
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
|
||||
<span><h3>InvenTree</h3></span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class='container-fluid'>
|
||||
<p>{% trans "You have been logged out" %}</p>
|
||||
<p><a href='{% url "login" %}'>{% trans "Return to login screen" %}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
@ -89,7 +89,12 @@
|
||||
<button class='pull-right btn btn-primary login-button' type="submit">{% trans "Login" %}</button>
|
||||
|
||||
</form>
|
||||
|
||||
{% if email_configured %}
|
||||
<hr><br>
|
||||
<p>{% trans "Forgotten your password?" %} - <a href='{% url "password_reset" %}'>{% trans "Click here to reset" %}</a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h4>{% trans "Logout" %}</h4>
|
||||
<p>{% trans "You have been logged out" %}</p>
|
||||
<p>{% trans 'Click' %} <a href="{% url 'login' %}"> {% trans 'here</a> to log in</p>' %}
|
||||
{% endblock %}
|
@ -0,0 +1,59 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-table.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
|
||||
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
|
||||
|
||||
<style>
|
||||
.login-error {
|
||||
color: #F88;
|
||||
}
|
||||
</style>
|
||||
|
||||
<title>
|
||||
InvenTree
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body class='login-screen'>
|
||||
|
||||
<div class='main body-wrapper login-screen'>
|
||||
|
||||
<div class='login-container'>
|
||||
<div class="row">
|
||||
<div class='container-fluid'>
|
||||
<div class='clearfix content-heading login-header'>
|
||||
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
|
||||
<span><h3>InvenTree</h3></span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class='container-fluid'>
|
||||
<p>{% trans "Password reset complete" %}</p>
|
||||
<p><a href='{% url "login" %}'>{% trans "Return to login screen" %}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
69
InvenTree/templates/registration/password_reset_confirm.html
Normal file
69
InvenTree/templates/registration/password_reset_confirm.html
Normal file
@ -0,0 +1,69 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-table.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
|
||||
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
|
||||
|
||||
<style>
|
||||
.login-error {
|
||||
color: #F88;
|
||||
}
|
||||
</style>
|
||||
|
||||
<title>
|
||||
InvenTree
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body class='login-screen'>
|
||||
|
||||
<div class='main body-wrapper login-screen'>
|
||||
|
||||
<div class='login-container'>
|
||||
<div class="row">
|
||||
<div class='container-fluid'>
|
||||
<div class='clearfix content-heading login-header'>
|
||||
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
|
||||
<span><h3>InvenTree</h3></span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class='container-fluid'>
|
||||
|
||||
{% if validlink %}
|
||||
<h3>{% trans "Change password" %}</h3>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Change password" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>
|
||||
{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
65
InvenTree/templates/registration/password_reset_done.html
Normal file
65
InvenTree/templates/registration/password_reset_done.html
Normal file
@ -0,0 +1,65 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-table.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
|
||||
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
|
||||
|
||||
<style>
|
||||
.login-error {
|
||||
color: #F88;
|
||||
}
|
||||
</style>
|
||||
|
||||
<title>
|
||||
InvenTree
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body class='login-screen'>
|
||||
|
||||
<div class='main body-wrapper login-screen'>
|
||||
|
||||
<div class='login-container'>
|
||||
<div class="row">
|
||||
<div class='container-fluid'>
|
||||
<div class='clearfix content-heading login-header'>
|
||||
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
|
||||
<span><h3>InvenTree</h3></span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class='container-fluid'>
|
||||
|
||||
<p>
|
||||
{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
<a href='{% url "login" %}'>{% trans "Return to login screen" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
68
InvenTree/templates/registration/password_reset_form.html
Normal file
68
InvenTree/templates/registration/password_reset_form.html
Normal file
@ -0,0 +1,68 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-table.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
|
||||
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
|
||||
|
||||
<style>
|
||||
.login-error {
|
||||
color: #F88;
|
||||
}
|
||||
</style>
|
||||
|
||||
<title>
|
||||
InvenTree
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body class='login-screen'>
|
||||
|
||||
<div class='main body-wrapper login-screen'>
|
||||
|
||||
<div class='login-container'>
|
||||
<div class="row">
|
||||
<div class='container-fluid'>
|
||||
<div class='clearfix content-heading login-header'>
|
||||
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
|
||||
<span><h3>InvenTree</h3></span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class='container-fluid'>
|
||||
|
||||
<p>{% trans "Forgotten your password?" %}</p>
|
||||
<p>{% trans "Enter your email address below." %}</p>
|
||||
<p>{% trans "An email will be sent with password reset instructions." %}</p>
|
||||
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Send email" %}</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
@ -25,18 +25,29 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if not django_q_running %}
|
||||
<tr>
|
||||
<td><span class='fas fa-tasks'></span></td>
|
||||
<td>{% trans "Background Worker" %}</td>
|
||||
<td>
|
||||
{% if django_q_running %}
|
||||
<span class='label label-green'>{% trans "Operational" %}</span>
|
||||
{% else %}
|
||||
<span class='label label-red'>{% trans "Not running" %}</span>
|
||||
{% endif %}
|
||||
<a href='https://inventree.readthedocs.io/en/latest/admin/tasks'>
|
||||
<span class='label label-red'>{% trans "Background worker not running" %}</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if not email_configured %}
|
||||
<tr>
|
||||
<td><span class='fas fa-envelope'></span></td>
|
||||
<td>{% trans "Email Settings" %}</td>
|
||||
<td>
|
||||
<a href='https://inventree.readthedocs.io/en/latest/admin/email'>
|
||||
<span class='label label-red'>{% trans "Email settings not configured" %}</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not system_healthy %}
|
||||
{% for issue in system_issues %}
|
||||
|
Loading…
Reference in New Issue
Block a user