mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into matmair/issue2279
This commit is contained in:
commit
acbee38600
6
.github/workflows/qc_checks.yaml
vendored
6
.github/workflows/qc_checks.yaml
vendored
@ -12,7 +12,7 @@ on:
|
|||||||
- l10*
|
- l10*
|
||||||
|
|
||||||
env:
|
env:
|
||||||
python_version: 3.7
|
python_version: 3.8
|
||||||
node_version: 16
|
node_version: 16
|
||||||
|
|
||||||
server_start_sleep: 60
|
server_start_sleep: 60
|
||||||
@ -229,6 +229,7 @@ jobs:
|
|||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
sudo apt-get install libpq-dev
|
sudo apt-get install libpq-dev
|
||||||
pip3 install invoke
|
pip3 install invoke
|
||||||
pip3 install psycopg2
|
pip3 install psycopg2
|
||||||
@ -282,7 +283,8 @@ jobs:
|
|||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install mysql-server libmysqlclient-dev
|
sudo apt-get update
|
||||||
|
sudo apt-get install libmysqlclient-dev
|
||||||
pip3 install invoke
|
pip3 install invoke
|
||||||
pip3 install mysqlclient
|
pip3 install mysqlclient
|
||||||
invoke install
|
invoke install
|
||||||
|
@ -4,12 +4,15 @@ Helper forms which subclass Django forms to provide additional functionality
|
|||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from urllib.parse import urlencode
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User, Group
|
from django.contrib.auth.models import User, Group
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Field
|
from crispy_forms.layout import Layout, Field
|
||||||
@ -18,6 +21,9 @@ from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppende
|
|||||||
from allauth.account.forms import SignupForm, set_form_field_order
|
from allauth.account.forms import SignupForm, set_form_field_order
|
||||||
from allauth.account.adapter import DefaultAccountAdapter
|
from allauth.account.adapter import DefaultAccountAdapter
|
||||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||||
|
from allauth.exceptions import ImmediateHttpResponse
|
||||||
|
from allauth_2fa.adapter import OTPAdapter
|
||||||
|
from allauth_2fa.utils import user_has_valid_totp_device
|
||||||
|
|
||||||
from part.models import PartCategory
|
from part.models import PartCategory
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
@ -278,7 +284,7 @@ class RegistratonMixin:
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
class CustomAccountAdapter(RegistratonMixin, DefaultAccountAdapter):
|
class CustomAccountAdapter(RegistratonMixin, OTPAdapter, DefaultAccountAdapter):
|
||||||
"""
|
"""
|
||||||
Override of adapter to use dynamic settings
|
Override of adapter to use dynamic settings
|
||||||
"""
|
"""
|
||||||
@ -297,3 +303,27 @@ class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter):
|
|||||||
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO', True):
|
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO', True):
|
||||||
return super().is_auto_signup_allowed(request, sociallogin)
|
return super().is_auto_signup_allowed(request, sociallogin)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# from OTPAdapter
|
||||||
|
def has_2fa_enabled(self, user):
|
||||||
|
"""Returns True if the user has 2FA configured."""
|
||||||
|
return user_has_valid_totp_device(user)
|
||||||
|
|
||||||
|
def login(self, request, user):
|
||||||
|
# Require two-factor authentication if it has been configured.
|
||||||
|
if self.has_2fa_enabled(user):
|
||||||
|
# Cast to string for the case when this is not a JSON serializable
|
||||||
|
# object, e.g. a UUID.
|
||||||
|
request.session['allauth_2fa_user_id'] = str(user.id)
|
||||||
|
|
||||||
|
redirect_url = reverse('two-factor-authenticate')
|
||||||
|
# Add GET parameters to the URL if they exist.
|
||||||
|
if request.GET:
|
||||||
|
redirect_url += u'?' + urlencode(request.GET)
|
||||||
|
|
||||||
|
raise ImmediateHttpResponse(
|
||||||
|
response=HttpResponseRedirect(redirect_url)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Otherwise defer to the original allauth adapter.
|
||||||
|
return super().login(request, user)
|
||||||
|
36
InvenTree/InvenTree/management/commands/remove_mfa.py
Normal file
36
InvenTree/InvenTree/management/commands/remove_mfa.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""
|
||||||
|
Custom management command to remove MFA for a user
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""
|
||||||
|
Remove MFA for a user
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('mail', type=str)
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# general settings
|
||||||
|
mail = kwargs.get('mail')
|
||||||
|
if not mail:
|
||||||
|
raise KeyError('A mail is required')
|
||||||
|
user = get_user_model()
|
||||||
|
mfa_user = [*set(user.objects.filter(email=mail) | user.objects.filter(emailaddress__email=mail))]
|
||||||
|
|
||||||
|
if len(mfa_user) == 0:
|
||||||
|
print('No user with this mail associated')
|
||||||
|
elif len(mfa_user) > 1:
|
||||||
|
print('More than one user found with this mail')
|
||||||
|
else:
|
||||||
|
# and clean out all MFA methods
|
||||||
|
# backup codes
|
||||||
|
mfa_user[0].staticdevice_set.all().delete()
|
||||||
|
# TOTP tokens
|
||||||
|
mfa_user[0].totpdevice_set.all().delete()
|
||||||
|
print(f'Removed all MFA methods for user {str(mfa_user[0])}')
|
@ -1,12 +1,18 @@
|
|||||||
from django.shortcuts import HttpResponseRedirect
|
from django.shortcuts import HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy, Resolver404
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.conf.urls import include, url
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
from allauth_2fa.middleware import BaseRequire2FAMiddleware, AllauthTwoFactorMiddleware
|
||||||
|
|
||||||
|
from InvenTree.urls import frontendpatterns
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
@ -146,3 +152,28 @@ class QueryCountMiddleware(object):
|
|||||||
print(x[0], ':', x[1])
|
print(x[0], ':', x[1])
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
url_matcher = url('', include(frontendpatterns))
|
||||||
|
|
||||||
|
|
||||||
|
class Check2FAMiddleware(BaseRequire2FAMiddleware):
|
||||||
|
"""check if user is required to have MFA enabled"""
|
||||||
|
def require_2fa(self, request):
|
||||||
|
# Superusers are require to have 2FA.
|
||||||
|
try:
|
||||||
|
if url_matcher.resolve(request.path[1:]):
|
||||||
|
return InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA')
|
||||||
|
except Resolver404:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class CustomAllauthTwoFactorMiddleware(AllauthTwoFactorMiddleware):
|
||||||
|
"""This function ensures only frontend code triggers the MFA auth cycle"""
|
||||||
|
def process_request(self, request):
|
||||||
|
try:
|
||||||
|
if not url_matcher.resolve(request.path[1:]):
|
||||||
|
super().process_request(request)
|
||||||
|
except Resolver404:
|
||||||
|
pass
|
||||||
|
@ -313,6 +313,12 @@ INSTALLED_APPS = [
|
|||||||
'allauth', # Base app for SSO
|
'allauth', # Base app for SSO
|
||||||
'allauth.account', # Extend user with accounts
|
'allauth.account', # Extend user with accounts
|
||||||
'allauth.socialaccount', # Use 'social' providers
|
'allauth.socialaccount', # Use 'social' providers
|
||||||
|
|
||||||
|
'django_otp', # OTP is needed for MFA - base package
|
||||||
|
'django_otp.plugins.otp_totp', # Time based OTP
|
||||||
|
'django_otp.plugins.otp_static', # Backup codes
|
||||||
|
|
||||||
|
'allauth_2fa', # MFA flow for allauth
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = CONFIG.get('middleware', [
|
MIDDLEWARE = CONFIG.get('middleware', [
|
||||||
@ -323,9 +329,12 @@ MIDDLEWARE = CONFIG.get('middleware', [
|
|||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django_otp.middleware.OTPMiddleware', # MFA support
|
||||||
|
'InvenTree.middleware.CustomAllauthTwoFactorMiddleware', # Flow control for allauth
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'InvenTree.middleware.AuthRequiredMiddleware',
|
'InvenTree.middleware.AuthRequiredMiddleware',
|
||||||
|
'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA
|
||||||
'maintenance_mode.middleware.MaintenanceModeMiddleware',
|
'maintenance_mode.middleware.MaintenanceModeMiddleware',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -131,15 +131,26 @@ translated_javascript_urls = [
|
|||||||
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
|
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
backendpatterns = [
|
||||||
url(r'^part/', include(part_urls)),
|
|
||||||
url(r'^manufacturer-part/', include(manufacturer_part_urls)),
|
|
||||||
url(r'^supplier-part/', include(supplier_part_urls)),
|
|
||||||
|
|
||||||
# "Dynamic" javascript files which are rendered using InvenTree templating.
|
# "Dynamic" javascript files which are rendered using InvenTree templating.
|
||||||
url(r'^js/dynamic/', include(dynamic_javascript_urls)),
|
url(r'^js/dynamic/', include(dynamic_javascript_urls)),
|
||||||
url(r'^js/i18n/', include(translated_javascript_urls)),
|
url(r'^js/i18n/', include(translated_javascript_urls)),
|
||||||
|
|
||||||
|
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
|
url(r'^auth/?', auth_request),
|
||||||
|
|
||||||
|
url(r'^api/', include(apipatterns)),
|
||||||
|
url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
|
||||||
|
|
||||||
|
# 3rd party endpoints
|
||||||
|
url(r'^markdownx/', include('markdownx.urls')),
|
||||||
|
]
|
||||||
|
|
||||||
|
frontendpatterns = [
|
||||||
|
url(r'^part/', include(part_urls)),
|
||||||
|
url(r'^manufacturer-part/', include(manufacturer_part_urls)),
|
||||||
|
url(r'^supplier-part/', include(supplier_part_urls)),
|
||||||
|
|
||||||
url(r'^common/', include(common_urls)),
|
url(r'^common/', include(common_urls)),
|
||||||
|
|
||||||
url(r'^stock/', include(stock_urls)),
|
url(r'^stock/', include(stock_urls)),
|
||||||
@ -149,8 +160,6 @@ urlpatterns = [
|
|||||||
|
|
||||||
url(r'^build/', include(build_urls)),
|
url(r'^build/', include(build_urls)),
|
||||||
|
|
||||||
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
|
|
||||||
|
|
||||||
url(r'^settings/', include(settings_urls)),
|
url(r'^settings/', include(settings_urls)),
|
||||||
|
|
||||||
url(r'^notifications/', include(notifications_urls)),
|
url(r'^notifications/', include(notifications_urls)),
|
||||||
@ -158,23 +167,17 @@ urlpatterns = [
|
|||||||
url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
|
url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
|
||||||
url(r'^set-password/', SetPasswordView.as_view(), name='set-password'),
|
url(r'^set-password/', SetPasswordView.as_view(), name='set-password'),
|
||||||
|
|
||||||
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'^index/', IndexView.as_view(), name='index'),
|
url(r'^index/', IndexView.as_view(), name='index'),
|
||||||
url(r'^search/', SearchView.as_view(), name='search'),
|
url(r'^search/', SearchView.as_view(), name='search'),
|
||||||
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
||||||
|
|
||||||
url(r'^auth/?', auth_request),
|
|
||||||
|
|
||||||
url(r'^api/', include(apipatterns)),
|
|
||||||
url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
|
|
||||||
|
|
||||||
# plugin urls
|
# plugin urls
|
||||||
get_plugin_urls(), # appends currently loaded plugin urls = None
|
get_plugin_urls(), # appends currently loaded plugin urls = None
|
||||||
|
|
||||||
url(r'^markdownx/', include('markdownx.urls')),
|
# admin sites
|
||||||
|
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'),
|
||||||
|
|
||||||
# DB user sessions
|
# DB user sessions
|
||||||
url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
|
url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
|
||||||
@ -185,7 +188,13 @@ urlpatterns = [
|
|||||||
url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
|
url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
|
||||||
url(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'),
|
url(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'),
|
||||||
url(r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"),
|
url(r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"),
|
||||||
url(r'^accounts/', include('allauth.urls')), # included urlpatterns
|
url(r'^accounts/', include('allauth_2fa.urls')), # MFA support
|
||||||
|
url(r'^accounts/', include('allauth.urls')), # included urlpatterns
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url('', include(frontendpatterns)),
|
||||||
|
url('', include(backendpatterns)),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Server running in "DEBUG" mode?
|
# Server running in "DEBUG" mode?
|
||||||
|
@ -962,6 +962,12 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'default': '',
|
'default': '',
|
||||||
'choices': settings_group_options
|
'choices': settings_group_options
|
||||||
},
|
},
|
||||||
|
'LOGIN_ENFORCE_MFA': {
|
||||||
|
'name': _('Enforce MFA'),
|
||||||
|
'description': _('Users must use multifactor security.'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
'ENABLE_PLUGINS_URL': {
|
'ENABLE_PLUGINS_URL': {
|
||||||
'name': _('Enable URL integration'),
|
'name': _('Enable URL integration'),
|
||||||
'description': _('Enable plugins to add URL routes'),
|
'description': _('Enable plugins to add URL routes'),
|
||||||
|
@ -11,7 +11,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from mptt.fields import TreeNodeChoiceField
|
from mptt.fields import TreeNodeChoiceField
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from InvenTree.helpers import GetExportFormats, clean_decimal
|
from InvenTree.helpers import clean_decimal
|
||||||
from InvenTree.fields import RoundingDecimalFormField
|
from InvenTree.fields import RoundingDecimalFormField
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
@ -55,36 +55,6 @@ class PartImageDownloadForm(HelperForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BomExportForm(forms.Form):
|
|
||||||
""" Simple form to let user set BOM export options,
|
|
||||||
before exporting a BOM (bill of materials) file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
file_format = forms.ChoiceField(label=_("File Format"), help_text=_("Select output file format"))
|
|
||||||
|
|
||||||
cascading = forms.BooleanField(label=_("Cascading"), required=False, initial=True, help_text=_("Download cascading / multi-level BOM"))
|
|
||||||
|
|
||||||
levels = forms.IntegerField(label=_("Levels"), required=True, initial=0, help_text=_("Select maximum number of BOM levels to export (0 = all levels)"))
|
|
||||||
|
|
||||||
parameter_data = forms.BooleanField(label=_("Include Parameter Data"), required=False, initial=False, help_text=_("Include part parameters data in exported BOM"))
|
|
||||||
|
|
||||||
stock_data = forms.BooleanField(label=_("Include Stock Data"), required=False, initial=False, help_text=_("Include part stock data in exported BOM"))
|
|
||||||
|
|
||||||
manufacturer_data = forms.BooleanField(label=_("Include Manufacturer Data"), required=False, initial=True, help_text=_("Include part manufacturer data in exported BOM"))
|
|
||||||
|
|
||||||
supplier_data = forms.BooleanField(label=_("Include Supplier Data"), required=False, initial=True, help_text=_("Include part supplier data in exported BOM"))
|
|
||||||
|
|
||||||
def get_choices(self):
|
|
||||||
""" BOM export format choices """
|
|
||||||
|
|
||||||
return [(x, x.upper()) for x in GetExportFormats()]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.fields['file_format'].choices = self.get_choices()
|
|
||||||
|
|
||||||
|
|
||||||
class BomDuplicateForm(HelperForm):
|
class BomDuplicateForm(HelperForm):
|
||||||
"""
|
"""
|
||||||
Simple confirmation form for BOM duplication.
|
Simple confirmation form for BOM duplication.
|
||||||
|
@ -475,9 +475,9 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
validated = serializers.BooleanField(read_only=True, source='is_line_valid')
|
validated = serializers.BooleanField(read_only=True, source='is_line_valid')
|
||||||
|
|
||||||
purchase_price_min = MoneyField(max_digits=10, decimal_places=6, read_only=True)
|
purchase_price_min = MoneyField(max_digits=19, decimal_places=4, read_only=True)
|
||||||
|
|
||||||
purchase_price_max = MoneyField(max_digits=10, decimal_places=6, read_only=True)
|
purchase_price_max = MoneyField(max_digits=19, decimal_places=4, read_only=True)
|
||||||
|
|
||||||
purchase_price_avg = serializers.SerializerMethodField()
|
purchase_price_avg = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
<div class='alert alert-info alert-block'>
|
<div class='alert alert-info alert-block'>
|
||||||
<strong>{% trans "Requirements for BOM upload" %}:</strong>
|
<strong>{% trans "Requirements for BOM upload" %}:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
<li>{% trans "The BOM file must contain the required named columns as provided in the " %} <strong><a href="/part/bom_template/">{% trans "BOM Upload Template" %}</a></strong></li>
|
<li>{% trans "The BOM file must contain the required named columns as provided in the " %} <strong><a href='#' id='bom-template-download'>{% trans "BOM Upload Template" %}</a></strong></li>
|
||||||
<li>{% trans "Each part must already exist in the database" %}</li>
|
<li>{% trans "Each part must already exist in the database" %}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -60,4 +60,8 @@
|
|||||||
|
|
||||||
enableSidebar('bom-upload');
|
enableSidebar('bom-upload');
|
||||||
|
|
||||||
{% endblock js_ready %}
|
$('#bom-template-download').click(function() {
|
||||||
|
downloadBomTemplate();
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endblock js_ready %}
|
@ -620,13 +620,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#download-bom").click(function () {
|
$("#download-bom").click(function () {
|
||||||
launchModalForm("{% url 'bom-export' part.id %}",
|
exportBom({{ part.id }});
|
||||||
{
|
|
||||||
success: function(response) {
|
|
||||||
location.href = response.url;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if report_enabled %}
|
{% if report_enabled %}
|
||||||
|
@ -1192,14 +1192,10 @@ class BomExport(AjaxView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
model = Part
|
model = Part
|
||||||
form_class = part_forms.BomExportForm
|
|
||||||
ajax_form_title = _("Export Bill of Materials")
|
ajax_form_title = _("Export Bill of Materials")
|
||||||
|
|
||||||
role_required = 'part.view'
|
role_required = 'part.view'
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
return self.renderJsonResponse(request, self.form_class())
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
# Extract POSTed form data
|
# Extract POSTed form data
|
||||||
|
@ -11,7 +11,7 @@ from importlib import reload
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url, include
|
||||||
from django.urls import clear_url_caches
|
from django.urls import clear_url_caches
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
@ -412,7 +412,7 @@ class Plugins:
|
|||||||
self.plugins_inactive = {}
|
self.plugins_inactive = {}
|
||||||
|
|
||||||
def _update_urls(self):
|
def _update_urls(self):
|
||||||
from InvenTree.urls import urlpatterns
|
from InvenTree.urls import urlpatterns as global_pattern, frontendpatterns as urlpatterns
|
||||||
from plugin.urls import get_plugin_urls
|
from plugin.urls import get_plugin_urls
|
||||||
|
|
||||||
for index, a in enumerate(urlpatterns):
|
for index, a in enumerate(urlpatterns):
|
||||||
@ -421,6 +421,9 @@ class Plugins:
|
|||||||
urlpatterns[index] = url(r'^admin/', admin.site.urls, name='inventree-admin')
|
urlpatterns[index] = url(r'^admin/', admin.site.urls, name='inventree-admin')
|
||||||
elif a.app_name == 'plugin':
|
elif a.app_name == 'plugin':
|
||||||
urlpatterns[index] = get_plugin_urls()
|
urlpatterns[index] = get_plugin_urls()
|
||||||
|
|
||||||
|
# replace frontendpatterns
|
||||||
|
global_pattern[0] = url('', include(urlpatterns))
|
||||||
clear_url_caches()
|
clear_url_caches()
|
||||||
|
|
||||||
def _reload_apps(self, force_reload: bool = False):
|
def _reload_apps(self, force_reload: bool = False):
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_SSO" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_SSO" icon="fa-info-circle" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" icon="fa-info-circle" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" icon="fa-info-circle" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENFORCE_MFA" %}
|
||||||
<tr>
|
<tr>
|
||||||
<th><h5>{% trans 'Signup' %}</h5></th>
|
<th><h5>{% trans 'Signup' %}</h5></th>
|
||||||
<td colspan='4'></td>
|
<td colspan='4'></td>
|
||||||
|
@ -42,193 +42,317 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class='panel-heading'>
|
<div class="row">
|
||||||
<div class='d-flex flex-span'>
|
<div class='panel-heading'>
|
||||||
|
<div class='d-flex flex-span'>
|
||||||
<h4>{% trans "Email" %}</h4>
|
<h4>{% trans "Email" %}</h4>
|
||||||
{% include "spacer.html" %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='row'>
|
<div class="col-sm-6">
|
||||||
<div class='col-sm-6'>
|
{% if user.emailaddress_set.all %}
|
||||||
{% if user.emailaddress_set.all %}
|
<p>{% trans 'The following email addresses are associated with your account:' %}</p>
|
||||||
<p>{% trans 'The following email addresses are associated with your account:' %}</p>
|
|
||||||
|
|
||||||
<form action="{% url 'account_email' %}" class="email_list" method="post">
|
<form action="{% url 'account_email' %}" class="email_list" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<fieldset class="blockLabels">
|
<fieldset class="blockLabels">
|
||||||
|
|
||||||
{% for emailaddress in user.emailaddress_set.all %}
|
{% for emailaddress in user.emailaddress_set.all %}
|
||||||
<div>
|
<div>
|
||||||
<div class="ctrlHolder">
|
<div class="ctrlHolder">
|
||||||
<label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}">
|
<label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}">
|
||||||
|
|
||||||
<input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked"{%endif %} value="{{emailaddress.email}}"/>
|
<input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked" {%endif %} value="{{emailaddress.email}}" />
|
||||||
|
|
||||||
{% if emailaddress.primary %}
|
{% if emailaddress.primary %}
|
||||||
<b>{{ emailaddress.email }}</b>
|
<b>{{ emailaddress.email }}</b>
|
||||||
{% else %}
|
|
||||||
{{ emailaddress.email }}
|
|
||||||
{% endif %}
|
|
||||||
</label>
|
|
||||||
{% if emailaddress.verified %}
|
|
||||||
<span class='badge badge-right rounded-pill bg-success'>{% trans "Verified" %}</span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class='badge badge-right rounded-pill bg-warning'>{% trans "Unverified" %}</span>
|
{{ emailaddress.email }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if emailaddress.primary %}<span class='badge badge-right rounded-pill bg-primary'>{% trans "Primary" %}</span>{% endif %}
|
</label>
|
||||||
</div>
|
|
||||||
|
{% if emailaddress.verified %}
|
||||||
|
<span class='badge badge-right rounded-pill bg-success'>{% trans "Verified" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class='badge badge-right rounded-pill bg-warning'>{% trans "Unverified" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if emailaddress.primary %}<span class='badge badge-right rounded-pill bg-primary'>{% trans "Primary" %}</span>{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="buttonHolder">
|
<div class="buttonHolder">
|
||||||
<button class="btn btn-primary secondaryAction" type="submit" name="action_primary" >{% trans 'Make Primary' %}</button>
|
<button class="btn btn-primary secondaryAction" type="submit" name="action_primary">{% trans 'Make Primary' %}</button>
|
||||||
<button class="btn btn-primary secondaryAction" type="submit" name="action_send" {% if not mail_conf %}disabled{% endif %}>{% trans 'Re-send Verification' %}</button>
|
<button class="btn btn-primary secondaryAction" type="submit" name="action_send" {% if not mail_conf %}disabled{% endif %}>{% trans 'Re-send Verification' %}</button>
|
||||||
<button class="btn btn-primary primaryAction" type="submit" name="action_remove" >{% trans 'Remove' %}</button>
|
<button class="btn btn-primary primaryAction" type="submit" name="action_remove">{% trans 'Remove' %}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class='alert alert-block alert-danger'>
|
||||||
|
<strong>{% trans 'Warning:'%}</strong>
|
||||||
|
{% trans "You currently do not have any email address set up. You should really add an email address so you can receive notifications, reset your password, etc." %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if can_add_email %}
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h5>{% trans "Add Email Address" %}</h5>
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'account_email' %}" class="add_email">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ add_email_form|crispy }}
|
||||||
|
<button class="btn btn-primary" name="action_add" type="submit">{% trans "Add Email" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class='panel-heading'>
|
||||||
|
<h4>{% trans "Social Accounts" %}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if social_form.accounts %}
|
||||||
|
<p>{% blocktrans %}You can sign in to your account using any of the following third party accounts:{% endblocktrans %}</p>
|
||||||
|
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'socialaccount_connections' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
{% if social_form.non_field_errors %}
|
||||||
|
<div id="errorMsg">{{ social_form.non_field_errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for base_account in social_form.accounts %}
|
||||||
|
{% with base_account.get_provider_account as account %}
|
||||||
|
<div>
|
||||||
|
<label for="id_account_{{ base_account.id }}">
|
||||||
|
<input id="id_account_{{ base_account.id }}" type="radio" name="account"
|
||||||
|
value="{{ base_account.id }}" />
|
||||||
|
<span class="socialaccount_provider {{ base_account.provider }} {{ account.get_brand.id }}">
|
||||||
|
<span class='brand-icon'
|
||||||
|
brand_name='{{account.get_brand.id}}'></span>{{account.get_brand.name}}</span>
|
||||||
|
{{ account }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary" type="submit">{% trans 'Remove' %}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><strong>{% trans 'Warning:'%}</strong>
|
<p>{% trans 'You currently have no social network accounts connected to this account.' %}</p>
|
||||||
{% trans "You currently do not have any email address set up. You should really add an email address so you can receive notifications, reset your password, etc." %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>{% trans 'Add a 3rd Party Account' %}</h5>
|
||||||
|
<div>
|
||||||
|
{% include "socialaccount/snippets/provider_list.html" with process="connect" %}
|
||||||
|
</div>
|
||||||
|
{% include "socialaccount/snippets/login_extra.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class='panel-heading'>
|
||||||
|
<h4>{% trans "Multifactor" %}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if user.staticdevice_set.all or user.totpdevice_set.all %}
|
||||||
|
<p>{% trans 'You have these factors available:' %}</p>
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Name</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for token in user.totpdevice_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'TOTP' %}</td>
|
||||||
|
<td>{{ token.name }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% for token in user.staticdevice_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Static' %}</td>
|
||||||
|
<td>{{ token.name }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<p><strong>{% trans 'Warning:'%}</strong>
|
||||||
|
{% trans "You currently do not have any factors set up." %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>{% trans "Change factors" %}</h5>
|
||||||
|
<a href="{% url 'two-factor-setup' %}" class="btn btn-primary {% if user.staticdevice_set.all and user.totpdevice_set.all %}disabled{% endif %}" role="button">{% trans "Setup multifactor" %}</a>
|
||||||
|
{% if user.staticdevice_set.all or user.totpdevice_set.all %}
|
||||||
|
<a href="{% url 'two-factor-remove' %}" class="btn btn-primary" role="button">{% trans "Remove multifactor" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<div class='panel-heading'>
|
||||||
|
<h4>{% trans "Theme Settings" %}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='col-sm-6'>
|
<div class='col-sm-6'>
|
||||||
{% if can_add_email %}
|
<form action='{% url "settings-appearance" %}' method='post'>
|
||||||
<h5>{% trans "Add Email Address" %}</h5>
|
|
||||||
|
|
||||||
<form method="post" action="{% url 'account_email' %}" class="add_email">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input name='next' type='hidden' value='{% url "settings" %}'>
|
||||||
<label for="id_email" class=" requiredField">
|
<label for='theme' class=' requiredField'>
|
||||||
E-mail<span class="asteriskField">*</span>
|
{% trans "Select theme" %}
|
||||||
</label>
|
</label>
|
||||||
<div id="div_id_email" class="form-group input-group mb-3">
|
<div class='form-group input-group mb-3'>
|
||||||
<div class='input-group-prepend'><span class='input-group-text'>@</span></div>
|
<select id='theme' name='theme' class='select form-control'>
|
||||||
<input type="email" name="email" placeholder='{% trans "Enter e-mail address" %}' class="textinput textInput form-control" required="" id="id_email">
|
{% get_available_themes as themes %}
|
||||||
|
{% for theme in themes %}
|
||||||
|
<option value='{{ theme.key }}'>{{ theme.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
<div class='input-group-append'>
|
<div class='input-group-append'>
|
||||||
<button class="btn btn-primary" name="action_add" type="submit">{% trans "Add Email" %}</button>
|
<input type="submit" value="{% trans 'Set Theme' %}" class="btn btn-primary">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='panel-heading'>
|
<div class="row">
|
||||||
<h4>{% trans "Social Accounts" %}</h4>
|
<div class='panel-heading'>
|
||||||
</div>
|
<h4>{% trans "Language Settings" %}</h4>
|
||||||
|
|
||||||
<div>
|
|
||||||
{% if social_form.accounts %}
|
|
||||||
<p>{% blocktrans %}You can sign in to your account using any of the following third party accounts:{% endblocktrans %}</p>
|
|
||||||
|
|
||||||
|
|
||||||
<form method="post" action="{% url 'socialaccount_connections' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
{% if social_form.non_field_errors %}
|
|
||||||
<div id="errorMsg">{{ social_form.non_field_errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for base_account in social_form.accounts %}
|
|
||||||
{% with base_account.get_provider_account as account %}
|
|
||||||
<div>
|
|
||||||
<label for="id_account_{{ base_account.id }}">
|
|
||||||
<input id="id_account_{{ base_account.id }}" type="radio" name="account" value="{{ base_account.id }}"/>
|
|
||||||
<span class="socialaccount_provider {{ base_account.provider }} {{ account.get_brand.id }}">
|
|
||||||
<span class='brand-icon' brand_name='{{account.get_brand.id}}'></span>{{account.get_brand.name}}</span>
|
|
||||||
{{ account }}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div>
|
<div class="col">
|
||||||
<button class="btn btn-primary" type="submit">{% trans 'Remove' %}</button>
|
<form action="{% url 'set_language' %}" method="post">
|
||||||
</div>
|
{% csrf_token %}
|
||||||
|
<input name="next" type="hidden" value="{% url 'settings' %}">
|
||||||
</fieldset>
|
<label for='language' class=' requiredField'>
|
||||||
|
{% trans "Select language" %}
|
||||||
|
</label>
|
||||||
|
<div class='form-group input-group mb-3'>
|
||||||
|
<select name="language" class="select form-control w-25">
|
||||||
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
|
{% get_available_languages as LANGUAGES %}
|
||||||
|
{% get_language_info_list for LANGUAGES as languages %}
|
||||||
|
{% if 'alllang' in request.GET %}{% define True as ALL_LANG %}{% endif %}
|
||||||
|
{% for language in languages %}
|
||||||
|
{% define language.code as lang_code %}
|
||||||
|
{% define locale_stats|keyvalue:lang_code as lang_translated %}
|
||||||
|
{% if lang_translated > 10 or lang_code == 'en' or lang_code == LANGUAGE_CODE %}{% define True as use_lang %}{% else %}{% define False as use_lang %}{% endif %}
|
||||||
|
{% if ALL_LANG or use_lang %}
|
||||||
|
<option value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
|
||||||
|
{{ language.name_local }} ({{ lang_code }})
|
||||||
|
{% if lang_translated %}
|
||||||
|
{% blocktrans %}{{ lang_translated }}% translated{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% if lang_code == 'en' %}-{% else %}{% trans 'No translations available' %}{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class='input-group-append'>
|
||||||
|
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
|
||||||
|
</div>
|
||||||
|
<p>{% trans "Some languages are not complete" %}
|
||||||
|
{% if ALL_LANG %}
|
||||||
|
. <a href="{% url 'settings' %}">{% trans "Show only sufficent" %}</a>
|
||||||
|
{% else %}
|
||||||
|
and hidden. <a href="?alllang">{% trans "Show them too" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% else %}
|
|
||||||
<div class='alert alert-block alert-warning'>
|
|
||||||
{% trans "There are no social network accounts connected to your InvenTree account" %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="col-sm-6">
|
||||||
|
<h4>{% trans "Help the translation efforts!" %}</h4>
|
||||||
<br>
|
<p>{% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the
|
||||||
<h4>{% trans 'Add a 3rd Party Account' %}</h4>
|
InvenTree web application is <a href="{{link}}">community contributed via crowdin</a>. Contributions are
|
||||||
<div>
|
welcomed and encouraged.{% endblocktrans %}</p>
|
||||||
{% include "socialaccount/snippets/provider_list.html" with process="connect" %}
|
|
||||||
</div>
|
</div>
|
||||||
{% include "socialaccount/snippets/login_extra.html" %}
|
|
||||||
<br>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='panel-heading'>
|
<div class="row">
|
||||||
<div class='d-flex flex-wrap'>
|
<div class='panel-heading'>
|
||||||
<h4>{% trans "Active Sessions" %}</h4>
|
<div class='d-flex flex-wrap'>
|
||||||
{% include "spacer.html" %}
|
<h4>{% trans "Active Sessions" %}</h4>
|
||||||
<div class='btn-group' role='group'>
|
{% include "spacer.html" %}
|
||||||
{% if session_list.count > 1 %}
|
<div class='btn-group' role='group'>
|
||||||
<form method="post" action="{% url 'session_delete_other' %}">
|
{% if session_list.count > 1 %}
|
||||||
{% csrf_token %}
|
<form method="post" action="{% url 'session_delete_other' %}">
|
||||||
<button type="submit" class="btn btn-sm btn-default btn-danger" title='{% trans "Log out active sessions (except this one)" %}'>
|
{% csrf_token %}
|
||||||
{% trans "Log Out Active Sessions" %}
|
<button type="submit" class="btn btn-sm btn-default btn-danger" title='{% trans "Log out active sessions (except this one)" %}'>
|
||||||
</button>
|
{% trans "Log Out Active Sessions" %}
|
||||||
</form>
|
</button>
|
||||||
{% endif %}
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% trans "<em>unknown on unknown</em>" as unknown_on_unknown %}
|
{% trans "<em>unknown on unknown</em>" as unknown_on_unknown %}
|
||||||
{% trans "<em>unknown</em>" as unknown %}
|
{% trans "<em>unknown</em>" as unknown %}
|
||||||
<table class="table table-striped table-condensed">
|
<table class="table table-striped table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "IP Address" %}</th>
|
<th>{% trans "IP Address" %}</th>
|
||||||
<th>{% trans "Device" %}</th>
|
<th>{% trans "Device" %}</th>
|
||||||
<th>{% trans "Last Activity" %}</th>
|
<th>{% trans "Last Activity" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% for object in session_list %}
|
{% for object in session_list %}
|
||||||
<tr {% if object.session_key == session_key %}class="active"{% endif %}>
|
<tr {% if object.session_key == session_key %}class="active"{% endif %}>
|
||||||
<td>{{ object.ip }}</td>
|
<td>{{ object.ip }}</td>
|
||||||
<td>{{ object.user_agent|device|default_if_none:unknown_on_unknown|safe }}</td>
|
<td>{{ object.user_agent|device|default_if_none:unknown_on_unknown|safe }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if object.session_key == session_key %}
|
{% if object.session_key == session_key %}
|
||||||
{% blocktrans with time=object.last_activity|timesince %}{{ time }} ago (this session){% endblocktrans %}
|
{% blocktrans with time=object.last_activity|timesince %}{{ time }} ago (this session){% endblocktrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% blocktrans with time=object.last_activity|timesince %}{{ time }} ago{% endblocktrans %}
|
{% blocktrans with time=object.last_activity|timesince %}{{ time }} ago{% endblocktrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
(function() {
|
(function() {
|
||||||
var message = "{% trans 'Do you really want to remove the selected email address?' %}";
|
var message = "{% trans 'Do you really want to remove the selected email address?' %}";
|
||||||
var actions = document.getElementsByName('action_remove');
|
var actions = document.getElementsByName('action_remove');
|
||||||
if (actions.length) {
|
if (actions.length) {
|
||||||
actions[0].addEventListener("click", function(e) {
|
actions[0].addEventListener("click", function(e) {
|
||||||
if (! confirm(message)) {
|
if (! confirm(message)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
{% endblock %}
|
{% endblock %}
|
15
InvenTree/templates/allauth_2fa/authenticate.html
Normal file
15
InvenTree/templates/allauth_2fa/authenticate.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "account/base.html" %}
|
||||||
|
{% load i18n crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Two-Factor Authentication" %}</h1>
|
||||||
|
|
||||||
|
<form method="post" class="login">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{% trans 'Authenticate' %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
33
InvenTree/templates/allauth_2fa/backup_tokens.html
Normal file
33
InvenTree/templates/allauth_2fa/backup_tokens.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% extends "account/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>
|
||||||
|
{% trans "Two-Factor Authentication Backup Tokens" %}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{% if backup_tokens %}
|
||||||
|
{% if reveal_tokens %}
|
||||||
|
<ul>
|
||||||
|
{% for token in backup_tokens %}
|
||||||
|
<li>{{ token.token }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
{% trans 'Backup tokens have been generated, but are not revealed here for security reasons. Press the button below to generate new ones.' %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'No tokens. Press the button below to generate some.' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
{% trans 'Generate backup tokens' %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
|
<a href="{% url 'settings' %}" class="btn btn-secondary w-100 btn-sm">{% trans "back to settings" %}</a>
|
||||||
|
|
||||||
|
{% endblock %}
|
18
InvenTree/templates/allauth_2fa/remove.html
Normal file
18
InvenTree/templates/allauth_2fa/remove.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% extends "account/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>
|
||||||
|
{% trans "Disable Two-Factor Authentication" %}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p>{% trans "Are you sure?" %}</p>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-danger w-100">
|
||||||
|
{% trans 'Disable Two-Factor' %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
42
InvenTree/templates/allauth_2fa/setup.html
Normal file
42
InvenTree/templates/allauth_2fa/setup.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{% extends "account/base.html" %}
|
||||||
|
{% load i18n crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>
|
||||||
|
{% trans "Setup Two-Factor Authentication" %}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<h4>
|
||||||
|
{% trans 'Step 1' %}:
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans 'Scan the QR code below with a token generator of your choice (for instance Google Authenticator).' %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-light rounded">
|
||||||
|
<img src="{{ qr_code_url }}" class="mx-auto d-block"/>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h4>
|
||||||
|
{% trans 'Step 2' %}:
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans 'Input a token generated by the app:' %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-block w-100">
|
||||||
|
{% trans 'Verify' %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'settings' %}" class="btn btn-secondary w-100 btn-sm mt-3">{% trans "back to settings" %}</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -175,7 +175,6 @@ function enableBreadcrumbTree(options) {
|
|||||||
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
node = data[i];
|
node = data[i];
|
||||||
node.nodes = [];
|
|
||||||
nodes[node.pk] = node;
|
nodes[node.pk] = node;
|
||||||
node.selectable = false;
|
node.selectable = false;
|
||||||
|
|
||||||
@ -193,10 +192,17 @@ function enableBreadcrumbTree(options) {
|
|||||||
node = data[i];
|
node = data[i];
|
||||||
|
|
||||||
if (node.parent != null) {
|
if (node.parent != null) {
|
||||||
nodes[node.parent].nodes.push(node);
|
if (nodes[node.parent].nodes) {
|
||||||
|
nodes[node.parent].nodes.push(node);
|
||||||
|
} else {
|
||||||
|
nodes[node.parent].nodes = [node];
|
||||||
|
}
|
||||||
|
|
||||||
if (node.state.expanded) {
|
if (node.state.expanded) {
|
||||||
nodes[node.parent].state.expanded = true;
|
while (node.parent != null) {
|
||||||
|
nodes[node.parent].state.expanded = true;
|
||||||
|
node = nodes[node.parent];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@ -212,7 +218,6 @@ function enableBreadcrumbTree(options) {
|
|||||||
collapseIcon: 'fa fa-chevron-down',
|
collapseIcon: 'fa fa-chevron-down',
|
||||||
});
|
});
|
||||||
|
|
||||||
setBreadcrumbTreeState(label, state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -220,26 +225,11 @@ function enableBreadcrumbTree(options) {
|
|||||||
$('#breadcrumb-tree-toggle').click(function() {
|
$('#breadcrumb-tree-toggle').click(function() {
|
||||||
// Add callback to "collapse" and "expand" the sidebar
|
// Add callback to "collapse" and "expand" the sidebar
|
||||||
|
|
||||||
// By default, the menu is "expanded"
|
// Toggle treeview visibilty
|
||||||
var state = localStorage.getItem(`inventree-tree-state-${label}`) || 'expanded';
|
$('#breadcrumb-tree-collapse').toggle();
|
||||||
|
|
||||||
// We wish to "toggle" the state!
|
|
||||||
setBreadcrumbTreeState(label, state == 'expanded' ? 'collapsed' : 'expanded');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set the initial state (default = expanded)
|
|
||||||
var state = localStorage.getItem(`inventree-tree-state-${label}`) || 'expanded';
|
|
||||||
|
|
||||||
function setBreadcrumbTreeState(label, state) {
|
|
||||||
|
|
||||||
if (state == 'collapsed') {
|
|
||||||
$('#breadcrumb-tree-collapse').hide(100);
|
|
||||||
} else {
|
|
||||||
$('#breadcrumb-tree-collapse').show(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(`inventree-tree-state-${label}`, state);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
/* globals
|
/* globals
|
||||||
constructForm,
|
constructForm,
|
||||||
|
exportFormatOptions,
|
||||||
imageHoverIcon,
|
imageHoverIcon,
|
||||||
inventreeGet,
|
inventreeGet,
|
||||||
inventreePut,
|
inventreePut,
|
||||||
@ -14,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
|
downloadBomTemplate,
|
||||||
|
exportBom,
|
||||||
newPartFromBomWizard,
|
newPartFromBomWizard,
|
||||||
loadBomTable,
|
loadBomTable,
|
||||||
loadUsedInTable,
|
loadUsedInTable,
|
||||||
@ -21,12 +24,121 @@
|
|||||||
removeColFromBomWizard,
|
removeColFromBomWizard,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* BOM management functions.
|
function downloadBomTemplate(options={}) {
|
||||||
* Requires follwing files to be loaded first:
|
|
||||||
* - api.js
|
var format = options.format;
|
||||||
* - part.js
|
|
||||||
* - modals.js
|
if (!format) {
|
||||||
|
format = inventreeLoad('bom-export-format', 'csv');
|
||||||
|
}
|
||||||
|
|
||||||
|
constructFormBody({}, {
|
||||||
|
title: '{% trans "Download BOM Template" %}',
|
||||||
|
fields: {
|
||||||
|
format: {
|
||||||
|
label: '{% trans "Format" %}',
|
||||||
|
help_text: '{% trans "Select file format" %}',
|
||||||
|
required: true,
|
||||||
|
type: 'choice',
|
||||||
|
value: format,
|
||||||
|
choices: exportFormatOptions(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmit: function(fields, opts) {
|
||||||
|
var format = getFormFieldValue('format', fields['format'], opts);
|
||||||
|
|
||||||
|
// Save the format for next time
|
||||||
|
inventreeSave('bom-export-format', format);
|
||||||
|
|
||||||
|
// Hide the modal
|
||||||
|
$(opts.modal).modal('hide');
|
||||||
|
|
||||||
|
// Download the file
|
||||||
|
location.href = `{% url "bom-upload-template" %}?format=${format}`;
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export BOM (Bill of Materials) for the specified Part instance
|
||||||
*/
|
*/
|
||||||
|
function exportBom(part_id, options={}) {
|
||||||
|
|
||||||
|
constructFormBody({}, {
|
||||||
|
title: '{% trans "Export BOM" %}',
|
||||||
|
fields: {
|
||||||
|
format: {
|
||||||
|
label: '{% trans "Format" %}',
|
||||||
|
help_text: '{% trans "Select file format" %}',
|
||||||
|
required: true,
|
||||||
|
type: 'choice',
|
||||||
|
value: inventreeLoad('bom-export-format', 'csv'),
|
||||||
|
choices: exportFormatOptions(),
|
||||||
|
},
|
||||||
|
cascading: {
|
||||||
|
label: '{% trans "Cascading" %}',
|
||||||
|
help_text: '{% trans "Download cascading / multi-level BOM" %}',
|
||||||
|
type: 'boolean',
|
||||||
|
value: inventreeLoad('bom-export-cascading', true),
|
||||||
|
},
|
||||||
|
levels: {
|
||||||
|
label: '{% trans "Levels" %}',
|
||||||
|
help_text: '{% trans "Select maximum number of BOM levels to export (0 = all levels)" %}',
|
||||||
|
type: 'integer',
|
||||||
|
value: 0,
|
||||||
|
min_value: 0,
|
||||||
|
},
|
||||||
|
parameter_data: {
|
||||||
|
label: '{% trans "Include Parameter Data" %}',
|
||||||
|
help_text: '{% trans "Include part parameter data in exported BOM" %}',
|
||||||
|
type: 'boolean',
|
||||||
|
value: inventreeLoad('bom-export-parameter_data', false),
|
||||||
|
},
|
||||||
|
stock_data: {
|
||||||
|
label: '{% trans "Include Stock Data" %}',
|
||||||
|
help_text: '{% trans "Include part stock data in exported BOM" %}',
|
||||||
|
type: 'boolean',
|
||||||
|
value: inventreeLoad('bom-export-stock_data', false),
|
||||||
|
},
|
||||||
|
manufacturer_data: {
|
||||||
|
label: '{% trans "Include Manufacturer Data" %}',
|
||||||
|
help_text: '{% trans "Include part manufacturer data in exported BOM" %}',
|
||||||
|
type: 'boolean',
|
||||||
|
value: inventreeLoad('bom-export-manufacturer_data', false),
|
||||||
|
},
|
||||||
|
supplier_data: {
|
||||||
|
label: '{% trans "Include Supplier Data" %}',
|
||||||
|
help_text: '{% trans "Include part supplier data in exported BOM" %}',
|
||||||
|
type: 'boolean',
|
||||||
|
value: inventreeLoad('bom-export-supplier_data', false),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmit: function(fields, opts) {
|
||||||
|
|
||||||
|
// Extract values from the form
|
||||||
|
var field_names = ['format', 'cascading', 'levels', 'parameter_data', 'stock_data', 'manufacturer_data', 'supplier_data'];
|
||||||
|
|
||||||
|
var url = `/part/${part_id}/bom-download/?`;
|
||||||
|
|
||||||
|
field_names.forEach(function(fn) {
|
||||||
|
var val = getFormFieldValue(fn, fields[fn], opts);
|
||||||
|
|
||||||
|
// Update user preferences
|
||||||
|
inventreeSave(`bom-export-${fn}`, val);
|
||||||
|
|
||||||
|
url += `${fn}=${val}&`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(opts.modal).modal('hide');
|
||||||
|
|
||||||
|
// Redirect to the BOM file download
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function bomItemFields() {
|
function bomItemFields() {
|
||||||
|
@ -811,7 +811,9 @@ function updateFieldValue(name, value, field, options) {
|
|||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
el.prop('checked', value);
|
if (value == true || value.toString().toLowerCase() == 'true') {
|
||||||
|
el.prop('checked');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'related field':
|
case 'related field':
|
||||||
// Clear?
|
// Clear?
|
||||||
@ -2034,8 +2036,15 @@ function constructInputOptions(name, classes, type, parameters) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parameters.value != null) {
|
if (parameters.value != null) {
|
||||||
// Existing value?
|
if (parameters.type == 'boolean') {
|
||||||
opts.push(`value='${parameters.value}'`);
|
// Special consideration of a boolean (checkbox) value
|
||||||
|
if (parameters.value == true || parameters.value.toString().toLowerCase() == 'true') {
|
||||||
|
opts.push('checked');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Existing value?
|
||||||
|
opts.push(`value='${parameters.value}'`);
|
||||||
|
}
|
||||||
} else if (parameters.default != null) {
|
} else if (parameters.default != null) {
|
||||||
// Otherwise, a defualt value?
|
// Otherwise, a defualt value?
|
||||||
opts.push(`value='${parameters.default}'`);
|
opts.push(`value='${parameters.default}'`);
|
||||||
|
@ -73,6 +73,9 @@ class RuleSet(models.Model):
|
|||||||
'socialaccount_socialaccount',
|
'socialaccount_socialaccount',
|
||||||
'socialaccount_socialapp',
|
'socialaccount_socialapp',
|
||||||
'socialaccount_socialtoken',
|
'socialaccount_socialtoken',
|
||||||
|
'otp_totp_totpdevice',
|
||||||
|
'otp_static_statictoken',
|
||||||
|
'otp_static_staticdevice',
|
||||||
'plugin_pluginconfig'
|
'plugin_pluginconfig'
|
||||||
],
|
],
|
||||||
'part_category': [
|
'part_category': [
|
||||||
@ -237,7 +240,7 @@ class RuleSet(models.Model):
|
|||||||
given the app_model name, and the permission type.
|
given the app_model name, and the permission type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
app, model = model.split('_')
|
model, app = split_model(model)
|
||||||
|
|
||||||
return "{app}.{perm}_{model}".format(
|
return "{app}.{perm}_{model}".format(
|
||||||
app=app,
|
app=app,
|
||||||
@ -280,6 +283,30 @@ class RuleSet(models.Model):
|
|||||||
return self.RULESET_MODELS.get(self.name, [])
|
return self.RULESET_MODELS.get(self.name, [])
|
||||||
|
|
||||||
|
|
||||||
|
def split_model(model):
|
||||||
|
"""get modelname and app from modelstring"""
|
||||||
|
*app, model = model.split('_')
|
||||||
|
|
||||||
|
# handle models that have
|
||||||
|
if len(app) > 1:
|
||||||
|
app = '_'.join(app)
|
||||||
|
else:
|
||||||
|
app = app[0]
|
||||||
|
|
||||||
|
return model, app
|
||||||
|
|
||||||
|
|
||||||
|
def split_permission(app, perm):
|
||||||
|
"""split permission string into permission and model"""
|
||||||
|
permission_name, *model = perm.split('_')
|
||||||
|
# handle models that have underscores
|
||||||
|
if len(model) > 1:
|
||||||
|
app += '_' + '_'.join(model[:-1])
|
||||||
|
perm = permission_name + '_' + model[-1:][0]
|
||||||
|
model = model[-1:][0]
|
||||||
|
return perm, model
|
||||||
|
|
||||||
|
|
||||||
def update_group_roles(group, debug=False):
|
def update_group_roles(group, debug=False):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -383,7 +410,7 @@ def update_group_roles(group, debug=False):
|
|||||||
|
|
||||||
(app, perm) = permission_string.split('.')
|
(app, perm) = permission_string.split('.')
|
||||||
|
|
||||||
(permission_name, model) = perm.split('_')
|
perm, model = split_permission(app, perm)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content_type = ContentType.objects.get(app_label=app, model=model)
|
content_type = ContentType.objects.get(app_label=app, model=model)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Please keep this list sorted
|
# Please keep this list sorted
|
||||||
Django==3.2.5 # Django package
|
Django==3.2.10 # Django package
|
||||||
certifi # Certifi is (most likely) installed through one of the requirements above
|
certifi # Certifi is (most likely) installed through one of the requirements above
|
||||||
coreapi==2.3.0 # API documentation
|
coreapi==2.3.0 # API documentation
|
||||||
coverage==5.3 # Unit test coverage
|
coverage==5.3 # Unit test coverage
|
||||||
@ -7,6 +7,7 @@ coveralls==2.1.2 # Coveralls linking (for Travis)
|
|||||||
cryptography==3.4.8 # Cryptography support
|
cryptography==3.4.8 # Cryptography support
|
||||||
django-admin-shell==0.1.2 # Python shell for the admin interface
|
django-admin-shell==0.1.2 # Python shell for the admin interface
|
||||||
django-allauth==0.45.0 # SSO for external providers via OpenID
|
django-allauth==0.45.0 # SSO for external providers via OpenID
|
||||||
|
django-allauth-2fa==0.8 # MFA / 2FA
|
||||||
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
|
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
|
||||||
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
||||||
django-crispy-forms==1.11.2 # Form helpers
|
django-crispy-forms==1.11.2 # Form helpers
|
||||||
|
12
tasks.py
12
tasks.py
@ -154,6 +154,18 @@ def clean_settings(c):
|
|||||||
manage(c, "clean_settings")
|
manage(c, "clean_settings")
|
||||||
|
|
||||||
|
|
||||||
|
@task(help={'mail': 'mail of the user whos MFA should be disabled'})
|
||||||
|
def remove_mfa(c, mail=''):
|
||||||
|
"""
|
||||||
|
Remove MFA for a user
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not mail:
|
||||||
|
print('You must provide a users mail')
|
||||||
|
|
||||||
|
manage(c, f"remove_mfa {mail}")
|
||||||
|
|
||||||
|
|
||||||
@task(post=[rebuild_models, rebuild_thumbnails])
|
@task(post=[rebuild_models, rebuild_thumbnails])
|
||||||
def migrate(c):
|
def migrate(c):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user