mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
9b9dfde158
4
.github/workflows/qc_checks.yaml
vendored
4
.github/workflows/qc_checks.yaml
vendored
@ -57,8 +57,8 @@ jobs:
|
||||
python3 check_js_templates.py
|
||||
- name: Lint Javascript Files
|
||||
run: |
|
||||
invoke render-js-files
|
||||
npx eslint js_tmp/*.js
|
||||
python InvenTree/manage.py prerender
|
||||
npx eslint InvenTree/InvenTree/static_i18n/i18n/*.js
|
||||
|
||||
html:
|
||||
name: Style [HTML]
|
||||
|
@ -15,6 +15,7 @@ from allauth.account.forms import SignupForm, set_form_field_order
|
||||
from allauth.exceptions import ImmediateHttpResponse
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from allauth_2fa.adapter import OTPAdapter
|
||||
from allauth_2fa.forms import TOTPDeviceRemoveForm
|
||||
from allauth_2fa.utils import user_has_valid_totp_device
|
||||
from crispy_forms.bootstrap import (AppendedText, PrependedAppendedText,
|
||||
PrependedText)
|
||||
@ -269,3 +270,36 @@ class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter):
|
||||
|
||||
# Otherwise defer to the original allauth adapter.
|
||||
return super().login(request, user)
|
||||
|
||||
|
||||
# Temporary fix for django-allauth-2fa # TODO remove
|
||||
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
|
||||
|
||||
class CustomTOTPDeviceRemoveForm(TOTPDeviceRemoveForm):
|
||||
"""Custom Form to ensure a token is provided before removing MFA"""
|
||||
# User must input a valid token so 2FA can be removed
|
||||
token = forms.CharField(
|
||||
label=_('Token'),
|
||||
)
|
||||
|
||||
def __init__(self, user, **kwargs):
|
||||
"""Add token field."""
|
||||
super().__init__(user, **kwargs)
|
||||
self.fields['token'].widget.attrs.update(
|
||||
{
|
||||
'autofocus': 'autofocus',
|
||||
'autocomplete': 'off',
|
||||
}
|
||||
)
|
||||
|
||||
def clean_token(self):
|
||||
"""Ensure at least one valid token is provided."""
|
||||
# Ensure that the user has provided a valid token
|
||||
token = self.cleaned_data.get('token')
|
||||
|
||||
# Verify that the user has provided a valid token
|
||||
for device in self.user.totpdevice_set.filter(confirmed=True):
|
||||
if device.verify_token(token):
|
||||
return token
|
||||
|
||||
raise forms.ValidationError(_("The entered token is not valid"))
|
||||
|
@ -4,9 +4,7 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
@ -27,18 +25,15 @@ class Command(BaseCommand):
|
||||
return
|
||||
|
||||
img = model.image
|
||||
url = img.thumbnail.name
|
||||
loc = os.path.join(settings.MEDIA_ROOT, url)
|
||||
|
||||
if not os.path.exists(loc):
|
||||
logger.info(f"Generating thumbnail image for '{img}'")
|
||||
logger.info(f"Generating thumbnail image for '{img}'")
|
||||
|
||||
try:
|
||||
model.image.render_variations(replace=False)
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"Warning: Image file '{img}' is missing")
|
||||
except UnidentifiedImageError:
|
||||
logger.warning(f"Warning: Image file '{img}' is not a valid image")
|
||||
try:
|
||||
model.image.render_variations(replace=False)
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"Warning: Image file '{img}' is missing")
|
||||
except UnidentifiedImageError:
|
||||
logger.warning(f"Warning: Image file '{img}' is not a valid image")
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Rebuild all thumbnail images."""
|
||||
|
@ -218,18 +218,6 @@ logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
||||
# Core django modules
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'user_sessions', # db user sessions
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
|
||||
# Maintenance
|
||||
'maintenance_mode',
|
||||
|
||||
# InvenTree apps
|
||||
'build.apps.BuildConfig',
|
||||
'common.apps.CommonConfig',
|
||||
@ -243,6 +231,18 @@ INSTALLED_APPS = [
|
||||
'plugin.apps.PluginAppConfig',
|
||||
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
||||
|
||||
# Core django modules
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'user_sessions', # db user sessions
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
|
||||
# Maintenance
|
||||
'maintenance_mode',
|
||||
|
||||
# Third part add-ons
|
||||
'django_filters', # Extended filter functionality
|
||||
'rest_framework', # DRF (Django Rest Framework)
|
||||
|
@ -16,6 +16,7 @@
|
||||
sanitizeData,
|
||||
*/
|
||||
|
||||
|
||||
function attachClipboard(selector, containerselector, textElement) {
|
||||
// set container
|
||||
if (containerselector) {
|
||||
|
128
InvenTree/InvenTree/static/script/inventree/message.js
Normal file
128
InvenTree/InvenTree/static/script/inventree/message.js
Normal file
@ -0,0 +1,128 @@
|
||||
/* exported
|
||||
showMessage,
|
||||
showAlertOrCache,
|
||||
showCachedAlerts,
|
||||
*/
|
||||
|
||||
/*
|
||||
* Display an alert message at the top of the screen.
|
||||
* The message will contain a "close" button,
|
||||
* and also dismiss automatically after a certain amount of time.
|
||||
*
|
||||
* arguments:
|
||||
* - message: Text / HTML content to display
|
||||
*
|
||||
* options:
|
||||
* - style: alert style e.g. 'success' / 'warning'
|
||||
* - timeout: Time (in milliseconds) after which the message will be dismissed
|
||||
*/
|
||||
function showMessage(message, options={}) {
|
||||
|
||||
var style = options.style || 'info';
|
||||
|
||||
var timeout = options.timeout || 5000;
|
||||
|
||||
var target = options.target || $('#alerts');
|
||||
|
||||
var details = '';
|
||||
|
||||
if (options.details) {
|
||||
details = `<p><small>${options.details}</p></small>`;
|
||||
}
|
||||
|
||||
// Hacky function to get the next available ID
|
||||
var id = 1;
|
||||
|
||||
while ($(`#alert-${id}`).exists()) {
|
||||
id++;
|
||||
}
|
||||
|
||||
var icon = '';
|
||||
|
||||
if (options.icon) {
|
||||
icon = `<span class='${options.icon}'></span>`;
|
||||
}
|
||||
|
||||
// Construct the alert
|
||||
var html = `
|
||||
<div id='alert-${id}' class='alert alert-${style} alert-dismissible fade show' role='alert'>
|
||||
${icon}
|
||||
<b>${message}</b>
|
||||
${details}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
target.append(html);
|
||||
|
||||
// Remove the alert automatically after a specified period of time
|
||||
$(`#alert-${id}`).delay(timeout).slideUp(200, function() {
|
||||
$(this).alert(close);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Add a cached alert message to sesion storage
|
||||
*/
|
||||
function addCachedAlert(message, options={}) {
|
||||
|
||||
var alerts = sessionStorage.getItem('inventree-alerts');
|
||||
|
||||
if (alerts) {
|
||||
alerts = JSON.parse(alerts);
|
||||
} else {
|
||||
alerts = [];
|
||||
}
|
||||
|
||||
alerts.push({
|
||||
message: message,
|
||||
style: options.style || 'success',
|
||||
icon: options.icon,
|
||||
});
|
||||
|
||||
sessionStorage.setItem('inventree-alerts', JSON.stringify(alerts));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Remove all cached alert messages
|
||||
*/
|
||||
function clearCachedAlerts() {
|
||||
sessionStorage.removeItem('inventree-alerts');
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display an alert, or cache to display on reload
|
||||
*/
|
||||
function showAlertOrCache(message, cache, options={}) {
|
||||
|
||||
if (cache) {
|
||||
addCachedAlert(message, options);
|
||||
} else {
|
||||
showMessage(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display cached alert messages when loading a page
|
||||
*/
|
||||
function showCachedAlerts() {
|
||||
|
||||
var alerts = JSON.parse(sessionStorage.getItem('inventree-alerts')) || [];
|
||||
|
||||
alerts.forEach(function(alert) {
|
||||
|
||||
showMessage(
|
||||
alert.message,
|
||||
{
|
||||
style: alert.style || 'success',
|
||||
icon: alert.icon,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
clearCachedAlerts();
|
||||
}
|
@ -28,10 +28,6 @@ class HTMLAPITests(InvenTreeTestCase):
|
||||
response = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check HTTP response
|
||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_build_api(self):
|
||||
"""Test that build list is working."""
|
||||
url = reverse('api-build-list')
|
||||
@ -40,10 +36,6 @@ class HTMLAPITests(InvenTreeTestCase):
|
||||
response = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check HTTP response
|
||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_stock_api(self):
|
||||
"""Test that stock list is working."""
|
||||
url = reverse('api-stock-list')
|
||||
@ -52,10 +44,6 @@ class HTMLAPITests(InvenTreeTestCase):
|
||||
response = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check HTTP response
|
||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_company_list(self):
|
||||
"""Test that company list is working."""
|
||||
url = reverse('api-company-list')
|
||||
@ -64,10 +52,6 @@ class HTMLAPITests(InvenTreeTestCase):
|
||||
response = self.client.get(url, HTTP_ACCEPT='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check HTTP response
|
||||
response = self.client.get(url, HTTP_ACCEPT='text/html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_not_found(self):
|
||||
"""Test that the NotFoundView is working."""
|
||||
response = self.client.get('/api/anc')
|
||||
|
@ -35,9 +35,9 @@ from .views import (AboutView, AppearanceSelectView, CurrencyRefreshView,
|
||||
CustomConnectionsView, CustomEmailView,
|
||||
CustomPasswordResetFromKeyView,
|
||||
CustomSessionDeleteOtherView, CustomSessionDeleteView,
|
||||
DatabaseStatsView, DynamicJsView, EditUserView, IndexView,
|
||||
NotificationsView, SearchView, SetPasswordView,
|
||||
SettingsView, auth_request)
|
||||
CustomTwoFactorRemove, DatabaseStatsView, DynamicJsView,
|
||||
EditUserView, IndexView, NotificationsView, SearchView,
|
||||
SetPasswordView, SettingsView, auth_request)
|
||||
|
||||
admin.site.site_header = "InvenTree Admin"
|
||||
|
||||
@ -164,6 +164,11 @@ frontendpatterns = [
|
||||
re_path(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
|
||||
re_path(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'),
|
||||
re_path(r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"),
|
||||
|
||||
# Temporary fix for django-allauth-2fa # TODO remove
|
||||
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
|
||||
re_path(r'^accounts/two_factor/remove/?$', CustomTwoFactorRemove.as_view(), name='two-factor-remove'),
|
||||
|
||||
re_path(r'^accounts/', include('allauth_2fa.urls')), # MFA support
|
||||
re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns
|
||||
]
|
||||
|
@ -28,6 +28,7 @@ from allauth.account.models import EmailAddress
|
||||
from allauth.account.views import EmailView, PasswordResetFromKeyView
|
||||
from allauth.socialaccount.forms import DisconnectForm
|
||||
from allauth.socialaccount.views import ConnectionsView
|
||||
from allauth_2fa.views import TwoFactorRemove
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||
from user_sessions.views import SessionDeleteOtherView, SessionDeleteView
|
||||
|
||||
@ -36,7 +37,7 @@ from common.settings import currency_code_default, currency_codes
|
||||
from part.models import PartCategory
|
||||
from users.models import RuleSet, check_user_role
|
||||
|
||||
from .forms import EditUserForm, SetPasswordForm
|
||||
from .forms import CustomTOTPDeviceRemoveForm, EditUserForm, SetPasswordForm
|
||||
|
||||
|
||||
def auth_request(request):
|
||||
@ -761,3 +762,12 @@ class NotificationsView(TemplateView):
|
||||
"""View for showing notifications."""
|
||||
|
||||
template_name = "InvenTree/notifications/notifications.html"
|
||||
|
||||
|
||||
# Temporary fix for django-allauth-2fa # TODO remove
|
||||
# See https://github.com/inventree/InvenTree/security/advisories/GHSA-8j76-mm54-52xq
|
||||
|
||||
class CustomTwoFactorRemove(TwoFactorRemove):
|
||||
"""Use custom form."""
|
||||
form_class = CustomTOTPDeviceRemoveForm
|
||||
success_url = reverse_lazy("settings")
|
||||
|
@ -4,6 +4,8 @@ import logging
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from InvenTree.ready import isImportingData
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
@ -26,6 +28,8 @@ class CommonConfig(AppConfig):
|
||||
|
||||
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED', backup_value=False, create=False, cache=False):
|
||||
logger.info("Clearing SERVER_RESTART_REQUIRED flag")
|
||||
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
|
||||
|
||||
if not isImportingData():
|
||||
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
|
||||
except Exception:
|
||||
pass
|
||||
|
@ -37,6 +37,7 @@ from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
import InvenTree.fields
|
||||
import InvenTree.helpers
|
||||
import InvenTree.ready
|
||||
import InvenTree.validators
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
@ -748,7 +749,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
"""
|
||||
super().save()
|
||||
|
||||
if self.requires_restart():
|
||||
if self.requires_restart() and not InvenTree.ready.isImportingData():
|
||||
InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', True, None)
|
||||
|
||||
"""
|
||||
|
@ -1318,6 +1318,11 @@ class Part(MetadataMixin, MPTTModel):
|
||||
|
||||
def allocation_count(self, **kwargs):
|
||||
"""Return the total quantity of stock allocated for this part, against both build orders and sales orders."""
|
||||
|
||||
if self.id is None:
|
||||
# If this instance has not been saved, foreign-key lookups will fail
|
||||
return 0
|
||||
|
||||
return sum(
|
||||
[
|
||||
self.build_order_allocation_count(**kwargs),
|
||||
|
@ -909,7 +909,7 @@
|
||||
},
|
||||
{% if 'price_diff' in price_history.0 %}
|
||||
{
|
||||
label: '{% blocktrans %}Unit Price-Cost Difference - {{currency}}{% endblocktrans %}',
|
||||
label: '{% blocktrans %}Unit Price-Cost Difference - {{ currency }}{% endblocktrans %}',
|
||||
backgroundColor: 'rgba(68, 157, 68, 0.2)',
|
||||
borderColor: 'rgb(68, 157, 68)',
|
||||
yAxisID: 'y2',
|
||||
@ -950,7 +950,7 @@
|
||||
{% if bom_parts %}
|
||||
var bom_colors = randomColor({hue: 'green', count: {{ bom_parts|length }} })
|
||||
var bomdata = {
|
||||
labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}],
|
||||
labels: [{% for line in bom_parts %}'{{ line.name|escapejs }}',{% endfor %}],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Price',
|
||||
|
121
InvenTree/part/templatetags/i18n.py
Normal file
121
InvenTree/part/templatetags/i18n.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""This module provides custom translation tags specifically for use with javascript code.
|
||||
|
||||
Translated strings are escaped, such that they can be used as string literals in a javascript file.
|
||||
"""
|
||||
|
||||
import django.templatetags.i18n
|
||||
from django import template
|
||||
from django.template import TemplateSyntaxError
|
||||
from django.templatetags.i18n import TranslateNode
|
||||
|
||||
import bleach
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
class CustomTranslateNode(TranslateNode):
|
||||
"""Custom translation node class, which sanitizes the translated strings for javascript use"""
|
||||
|
||||
def render(self, context):
|
||||
"""Custom render function overrides / extends default behaviour"""
|
||||
|
||||
result = super().render(context)
|
||||
|
||||
result = bleach.clean(result)
|
||||
|
||||
# Remove any escape sequences
|
||||
for seq in ['\a', '\b', '\f', '\n', '\r', '\t', '\v']:
|
||||
result = result.replace(seq, '')
|
||||
|
||||
# Remove other disallowed characters
|
||||
for c in ['\\', '`', ';', '|', '&']:
|
||||
result = result.replace(c, '')
|
||||
|
||||
# Escape any quotes contained in the string
|
||||
result = result.replace("'", r"\'")
|
||||
result = result.replace('"', r'\"')
|
||||
|
||||
# Return the 'clean' resulting string
|
||||
return result
|
||||
|
||||
|
||||
@register.tag("translate")
|
||||
@register.tag("trans")
|
||||
def do_translate(parser, token):
|
||||
"""Custom translation function, lifted from https://github.com/django/django/blob/main/django/templatetags/i18n.py
|
||||
|
||||
The only difference is that we pass this to our custom rendering node class
|
||||
"""
|
||||
|
||||
bits = token.split_contents()
|
||||
if len(bits) < 2:
|
||||
raise TemplateSyntaxError("'%s' takes at least one argument" % bits[0])
|
||||
message_string = parser.compile_filter(bits[1])
|
||||
remaining = bits[2:]
|
||||
|
||||
noop = False
|
||||
asvar = None
|
||||
message_context = None
|
||||
seen = set()
|
||||
invalid_context = {"as", "noop"}
|
||||
|
||||
while remaining:
|
||||
option = remaining.pop(0)
|
||||
if option in seen:
|
||||
raise TemplateSyntaxError(
|
||||
"The '%s' option was specified more than once." % option,
|
||||
)
|
||||
elif option == "noop":
|
||||
noop = True
|
||||
elif option == "context":
|
||||
try:
|
||||
value = remaining.pop(0)
|
||||
except IndexError:
|
||||
raise TemplateSyntaxError(
|
||||
"No argument provided to the '%s' tag for the context option."
|
||||
% bits[0]
|
||||
)
|
||||
if value in invalid_context:
|
||||
raise TemplateSyntaxError(
|
||||
"Invalid argument '%s' provided to the '%s' tag for the context "
|
||||
"option" % (value, bits[0]),
|
||||
)
|
||||
message_context = parser.compile_filter(value)
|
||||
elif option == "as":
|
||||
try:
|
||||
value = remaining.pop(0)
|
||||
except IndexError:
|
||||
raise TemplateSyntaxError(
|
||||
"No argument provided to the '%s' tag for the as option." % bits[0]
|
||||
)
|
||||
asvar = value
|
||||
else:
|
||||
raise TemplateSyntaxError(
|
||||
"Unknown argument for '%s' tag: '%s'. The only options "
|
||||
"available are 'noop', 'context' \"xxx\", and 'as VAR'."
|
||||
% (
|
||||
bits[0],
|
||||
option,
|
||||
)
|
||||
)
|
||||
seen.add(option)
|
||||
|
||||
return CustomTranslateNode(message_string, noop, asvar, message_context)
|
||||
|
||||
|
||||
# Re-register tags which we have not explicitly overridden
|
||||
register.tag("blocktrans", django.templatetags.i18n.do_block_translate)
|
||||
register.tag("blocktranslate", django.templatetags.i18n.do_block_translate)
|
||||
|
||||
register.tag("language", django.templatetags.i18n.language)
|
||||
|
||||
register.tag("get_available_languages", django.templatetags.i18n.do_get_available_languages)
|
||||
register.tag("get_language_info", django.templatetags.i18n.do_get_language_info)
|
||||
register.tag("get_language_info_list", django.templatetags.i18n.do_get_language_info_list)
|
||||
register.tag("get_current_language", django.templatetags.i18n.do_get_current_language)
|
||||
register.tag("get_current_language_bidi", django.templatetags.i18n.do_get_current_language_bidi)
|
||||
|
||||
register.filter("language_name", django.templatetags.i18n.language_name)
|
||||
register.filter("language_name_translated", django.templatetags.i18n.language_name_translated)
|
||||
register.filter("language_name_local", django.templatetags.i18n.language_name_local)
|
||||
register.filter("language_bidi", django.templatetags.i18n.language_bidi)
|
@ -89,27 +89,20 @@
|
||||
<!-- general JS -->
|
||||
{% include "third_party_js.html" %}
|
||||
|
||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
|
||||
|
||||
<script type='text/javascript' src='{% static "script/inventree/message.js" %}'></script>
|
||||
|
||||
<script type='text/javascript'>
|
||||
|
||||
$(document).ready(function () {
|
||||
// notifications
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
showAlertOrCache(
|
||||
'{{ message }}',
|
||||
true,
|
||||
{
|
||||
style: 'info',
|
||||
}
|
||||
);
|
||||
showMessage(messsage);
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
inventreeDocReady();
|
||||
showCachedAlerts();
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
@ -17,7 +17,10 @@
|
||||
|
||||
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary btn-block">{% trans 'Confirm' %}</button>
|
||||
<hr>
|
||||
<button type="submit" class="btn btn-success float-right">
|
||||
<span class='fas fa-check-circle'></span> {% trans 'Confirm' %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
@ -8,7 +8,8 @@
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<hr>
|
||||
<button type="submit" class="btn btn-primary float-right">
|
||||
<span class='fas fa-check-circle'></span> {% trans 'Authenticate' %}
|
||||
</button>
|
||||
</form>
|
||||
|
@ -141,6 +141,7 @@
|
||||
|
||||
<!-- general JS -->
|
||||
<script defer type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||
<script defer type='text/javascript' src="{% static 'script/inventree/message.js' %}"></script>
|
||||
|
||||
<!-- dynamic javascript templates -->
|
||||
<script defer type='text/javascript' src="{% url 'calendar.js' %}"></script>
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
/* exported
|
||||
loadNotificationTable,
|
||||
showAlertOrCache,
|
||||
showCachedAlerts,
|
||||
startNotificationWatcher,
|
||||
stopNotificationWatcher,
|
||||
openNotificationPanel,
|
||||
@ -100,128 +98,6 @@ function loadNotificationTable(table, options={}, enableDelete=false) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Add a cached alert message to sesion storage
|
||||
*/
|
||||
function addCachedAlert(message, options={}) {
|
||||
|
||||
var alerts = sessionStorage.getItem('inventree-alerts');
|
||||
|
||||
if (alerts) {
|
||||
alerts = JSON.parse(alerts);
|
||||
} else {
|
||||
alerts = [];
|
||||
}
|
||||
|
||||
alerts.push({
|
||||
message: message,
|
||||
style: options.style || 'success',
|
||||
icon: options.icon,
|
||||
});
|
||||
|
||||
sessionStorage.setItem('inventree-alerts', JSON.stringify(alerts));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Remove all cached alert messages
|
||||
*/
|
||||
function clearCachedAlerts() {
|
||||
sessionStorage.removeItem('inventree-alerts');
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display an alert, or cache to display on reload
|
||||
*/
|
||||
function showAlertOrCache(message, cache, options={}) {
|
||||
|
||||
if (cache) {
|
||||
addCachedAlert(message, options);
|
||||
} else {
|
||||
showMessage(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display cached alert messages when loading a page
|
||||
*/
|
||||
function showCachedAlerts() {
|
||||
|
||||
var alerts = JSON.parse(sessionStorage.getItem('inventree-alerts')) || [];
|
||||
|
||||
alerts.forEach(function(alert) {
|
||||
showMessage(
|
||||
alert.message,
|
||||
{
|
||||
style: alert.style || 'success',
|
||||
icon: alert.icon,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
clearCachedAlerts();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display an alert message at the top of the screen.
|
||||
* The message will contain a "close" button,
|
||||
* and also dismiss automatically after a certain amount of time.
|
||||
*
|
||||
* arguments:
|
||||
* - message: Text / HTML content to display
|
||||
*
|
||||
* options:
|
||||
* - style: alert style e.g. 'success' / 'warning'
|
||||
* - timeout: Time (in milliseconds) after which the message will be dismissed
|
||||
*/
|
||||
function showMessage(message, options={}) {
|
||||
|
||||
var style = options.style || 'info';
|
||||
|
||||
var timeout = options.timeout || 5000;
|
||||
|
||||
var target = options.target || $('#alerts');
|
||||
|
||||
var details = '';
|
||||
|
||||
if (options.details) {
|
||||
details = `<p><small>${options.details}</p></small>`;
|
||||
}
|
||||
|
||||
// Hacky function to get the next available ID
|
||||
var id = 1;
|
||||
|
||||
while ($(`#alert-${id}`).exists()) {
|
||||
id++;
|
||||
}
|
||||
|
||||
var icon = '';
|
||||
|
||||
if (options.icon) {
|
||||
icon = `<span class='${options.icon}'></span>`;
|
||||
}
|
||||
|
||||
// Construct the alert
|
||||
var html = `
|
||||
<div id='alert-${id}' class='alert alert-${style} alert-dismissible fade show' role='alert'>
|
||||
${icon}
|
||||
<b>${message}</b>
|
||||
${details}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
target.append(html);
|
||||
|
||||
// Remove the alert automatically after a specified period of time
|
||||
$(`#alert-${id}`).delay(timeout).slideUp(200, function() {
|
||||
$(this).alert(close);
|
||||
});
|
||||
}
|
||||
|
||||
var notificationWatcher = null; // reference for the notificationWatcher
|
||||
/**
|
||||
* start the regular notification checks
|
||||
|
@ -70,6 +70,7 @@
|
||||
|
||||
<!-- general JS -->
|
||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/inventree/message.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
|
||||
{% block body_scripts_inventree %}
|
||||
{% endblock %}
|
||||
|
@ -8,7 +8,7 @@ coveralls==2.1.2 # Coveralls linking (for Travis)
|
||||
cryptography==3.4.8 # Cryptography support
|
||||
django-admin-shell==0.1.2 # Python shell for the admin interface
|
||||
django-allauth==0.48.0 # SSO for external providers via OpenID
|
||||
django-allauth-2fa==0.9 # MFA / 2FA
|
||||
django-allauth-2fa==0.9 # MFA / 2FA # IMPORTANT: Do only change after reviewing GHSA-8j76-mm54-52xq
|
||||
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
|
||||
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
||||
django-crispy-forms==1.11.2 # Form helpers
|
||||
|
Loading…
Reference in New Issue
Block a user