totp: rename tfa to totp
This commit is contained in:
parent
52d1920914
commit
fbf58801ec
|
@ -74,7 +74,7 @@ INSTALLED_APPS = [
|
||||||
'passbook.oauth_client',
|
'passbook.oauth_client',
|
||||||
'passbook.oauth_provider',
|
'passbook.oauth_provider',
|
||||||
'passbook.saml_idp',
|
'passbook.saml_idp',
|
||||||
'passbook.tfa',
|
'passbook.totp',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Message Tag fix for bootstrap CSS Classes
|
# Message Tag fix for bootstrap CSS Classes
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
"""passbook tfa Header"""
|
|
||||||
__version__ = '0.0.1-alpha'
|
|
||||||
default_app_config = 'passbook.tfa.apps.PassbookTFAConfig'
|
|
|
@ -1,11 +0,0 @@
|
||||||
"""passbook 2FA AppConfig"""
|
|
||||||
|
|
||||||
from django.apps.config import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class PassbookTFAConfig(AppConfig):
|
|
||||||
"""passbook TFA AppConfig"""
|
|
||||||
|
|
||||||
name = 'passbook.tfa'
|
|
||||||
label = 'passbook_tfa'
|
|
||||||
mountpoint = 'user/tfa/'
|
|
|
@ -1,14 +0,0 @@
|
||||||
"""passbook 2FA Urls"""
|
|
||||||
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from passbook.tfa import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('', views.index, name='tfa-index'),
|
|
||||||
path('qr/', views.qr_code, name='tfa-qr'),
|
|
||||||
path('verify/', views.verify, name='tfa-verify'),
|
|
||||||
# path('enable/', views.TFASetupView.as_view(), name='tfa-enable'),
|
|
||||||
path('disable/', views.disable, name='tfa-disable'),
|
|
||||||
path('user_settings/', views.user_settings, name='tfa-user_settings'),
|
|
||||||
]
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
"""passbook totp Header"""
|
||||||
|
__version__ = '0.0.1-alpha'
|
||||||
|
default_app_config = 'passbook.totp.apps.PassbookTOTPConfig'
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""passbook TOTP AppConfig"""
|
||||||
|
|
||||||
|
from django.apps.config import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PassbookTOTPConfig(AppConfig):
|
||||||
|
"""passbook TOTP AppConfig"""
|
||||||
|
|
||||||
|
name = 'passbook.totp'
|
||||||
|
label = 'passbook_totp'
|
||||||
|
mountpoint = 'user/totp/'
|
|
@ -1,11 +1,11 @@
|
||||||
"""passbook 2FA Forms"""
|
"""passbook TOTP Forms"""
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
TFA_CODE_VALIDATOR = RegexValidator(r'^[0-9a-z]{6,8}$',
|
TOTP_CODE_VALIDATOR = RegexValidator(r'^[0-9a-z]{6,8}$',
|
||||||
_('Only alpha-numeric characters are allowed.'))
|
_('Only alpha-numeric characters are allowed.'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,37 +16,37 @@ class PictureWidget(forms.widgets.Widget):
|
||||||
return mark_safe("<img src=\"%s\" />" % value) # nosec
|
return mark_safe("<img src=\"%s\" />" % value) # nosec
|
||||||
|
|
||||||
|
|
||||||
class TFAVerifyForm(forms.Form):
|
class TOTPVerifyForm(forms.Form):
|
||||||
"""Simple Form to verify 2FA Code"""
|
"""Simple Form to verify TOTP Code"""
|
||||||
order = ['code']
|
order = ['code']
|
||||||
|
|
||||||
code = forms.CharField(label=_('Code'), validators=[TFA_CODE_VALIDATOR],
|
code = forms.CharField(label=_('Code'), validators=[TOTP_CODE_VALIDATOR],
|
||||||
widget=forms.TextInput(attrs={'autocomplete': 'off'}))
|
widget=forms.TextInput(attrs={'autocomplete': 'off'}))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(TFAVerifyForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# This is a little helper so the field is focused by default
|
# This is a little helper so the field is focused by default
|
||||||
self.fields['code'].widget.attrs.update({'autofocus': 'autofocus'})
|
self.fields['code'].widget.attrs.update({'autofocus': 'autofocus'})
|
||||||
|
|
||||||
|
|
||||||
class TFASetupInitForm(forms.Form):
|
class TOTPSetupInitForm(forms.Form):
|
||||||
"""Initial 2FA Setup form"""
|
"""Initial TOTP Setup form"""
|
||||||
title = _('Set up 2FA')
|
title = _('Set up TOTP')
|
||||||
device = None
|
device = None
|
||||||
confirmed = False
|
confirmed = False
|
||||||
qr_code = forms.CharField(widget=PictureWidget, disabled=True, required=False,
|
qr_code = forms.CharField(widget=PictureWidget, disabled=True, required=False,
|
||||||
label=_('Scan this Code with your 2FA App.'))
|
label=_('Scan this Code with your TOTP App.'))
|
||||||
code = forms.CharField(label=_('Code'), validators=[TFA_CODE_VALIDATOR])
|
code = forms.CharField(label=_('Code'), validators=[TOTP_CODE_VALIDATOR])
|
||||||
|
|
||||||
def clean_code(self):
|
def clean_code(self):
|
||||||
"""Check code with new totp device"""
|
"""Check code with new totp device"""
|
||||||
if self.device is not None:
|
if self.device is not None:
|
||||||
if not self.device.verify_token(int(self.cleaned_data.get('code'))) \
|
if not self.device.verify_token(int(self.cleaned_data.get('code'))) \
|
||||||
and not self.confirmed:
|
and not self.confirmed:
|
||||||
raise forms.ValidationError(_("2FA Code does not match"))
|
raise forms.ValidationError(_("TOTP Code does not match"))
|
||||||
return self.cleaned_data.get('code')
|
return self.cleaned_data.get('code')
|
||||||
|
|
||||||
|
|
||||||
class TFASetupStaticForm(forms.Form):
|
class TOTPSetupStaticForm(forms.Form):
|
||||||
"""Static form to show generated static tokens"""
|
"""Static form to show generated static tokens"""
|
||||||
tokens = forms.MultipleChoiceField(disabled=True, required=False)
|
tokens = forms.MultipleChoiceField(disabled=True, required=False)
|
|
@ -1,4 +1,4 @@
|
||||||
"""passbook 2FA Middleware to force users with 2FA set up to verify"""
|
"""passbook TOTP Middleware to force users with TOTP set up to verify"""
|
||||||
|
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
@ -6,24 +6,25 @@ from django.utils.http import urlencode
|
||||||
from django_otp import user_has_device
|
from django_otp import user_has_device
|
||||||
|
|
||||||
|
|
||||||
def tfa_force_verify(get_response):
|
def totp_force_verify(get_response):
|
||||||
"""Middleware to force 2FA Verification"""
|
"""Middleware to force TOTP Verification"""
|
||||||
|
|
||||||
def middleware(request):
|
def middleware(request):
|
||||||
"""Middleware to force 2FA Verification"""
|
"""Middleware to force TOTP Verification"""
|
||||||
|
|
||||||
# pylint: disable=too-many-boolean-expressions
|
# pylint: disable=too-many-boolean-expressions
|
||||||
if request.user.is_authenticated and \
|
if request.user.is_authenticated and \
|
||||||
user_has_device(request.user) and \
|
user_has_device(request.user) and \
|
||||||
not request.user.is_verified() and \
|
not request.user.is_verified() and \
|
||||||
request.path != reverse('passbook_tfa:tfa-verify') and \
|
request.path != reverse('passbook_totp:totp-verify') and \
|
||||||
request.path != reverse('account-logout') and \
|
request.path != reverse('account-logout') and \
|
||||||
not request.META.get('HTTP_AUTHORIZATION', '').startswith('Bearer'):
|
not request.META.get('HTTP_AUTHORIZATION', '').startswith('Bearer'):
|
||||||
# User has 2FA set up but is not verified
|
# User has TOTP set up but is not verified
|
||||||
|
|
||||||
# At this point the request is already forwarded to the target destination
|
# At this point the request is already forwarded to the target destination
|
||||||
# So we just add the current request's path as next parameter
|
# So we just add the current request's path as next parameter
|
||||||
args = '?%s' % urlencode({'next': request.get_full_path()})
|
args = '?%s' % urlencode({'next': request.get_full_path()})
|
||||||
return redirect(reverse('passbook_tfa:tfa-verify') + args)
|
return redirect(reverse('passbook_totp:totp-verify') + args)
|
||||||
|
|
||||||
response = get_response(request)
|
response = get_response(request)
|
||||||
return response
|
return response
|
|
@ -1,4 +1,4 @@
|
||||||
"""passbook 2FA Settings"""
|
"""passbook TOTP Settings"""
|
||||||
|
|
||||||
OTP_LOGIN_URL = 'passbook_tfa:tfa-verify'
|
OTP_LOGIN_URL = 'passbook_tfa:tfa-verify'
|
||||||
OTP_TOTP_ISSUER = 'passbook'
|
OTP_TOTP_ISSUER = 'passbook'
|
|
@ -31,9 +31,9 @@
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{% if not state %}
|
{% if not state %}
|
||||||
<a href="{% url 'passbook_tfa:tfa-enable' %}" class="btn btn-success btn-sm">{% trans "Enable 2FA" %}</a>
|
<a href="{% url 'passbook_tfa:tfa-enable' %}" class="btn btn-success btn-sm">{% trans "Enable TOTP" %}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'passbook_tfa:tfa-disable' %}" class="btn btn-danger btn-sm">{% trans "Disable 2FA" %}</a>
|
<a href="{% url 'passbook_tfa:tfa-disable' %}" class="btn btn-danger btn-sm">{% trans "Disable TOTP" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
|
@ -7,7 +7,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
<label for="">Keep these tokens somewhere safe. These are to be used if you loose your primary 2FA device.</label>
|
<label for="">Keep these tokens somewhere safe. These are to be used if you loose your primary TOTP device.</label>
|
||||||
{% for field in wizard.form %}
|
{% for field in wizard.form %}
|
||||||
{% if field.field.widget|fieldtype == 'SelectMultiple' %}
|
{% if field.field.widget|fieldtype == 'SelectMultiple' %}
|
||||||
<ul class="list">
|
<ul class="list">
|
|
@ -1,4 +1,4 @@
|
||||||
"""passbook Mod 2FA Middleware Test"""
|
"""passbook TOTP Middleware Test"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -7,19 +7,19 @@ from django.test import RequestFactory, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from passbook.core.views import overview
|
from passbook.core.views import overview
|
||||||
from passbook.tfa.middleware import tfa_force_verify
|
from passbook.totp.middleware import totp_force_verify
|
||||||
|
|
||||||
|
|
||||||
class TestMiddleware(TestCase):
|
class TestMiddleware(TestCase):
|
||||||
"""passbook 2FA Middleware Test"""
|
"""passbook TOTP Middleware Test"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
os.environ['RECAPTCHA_TESTING'] = 'True'
|
os.environ['RECAPTCHA_TESTING'] = 'True'
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
def test_tfa_force_verify_anon(self):
|
def test_totp_force_verify_anon(self):
|
||||||
"""Test Anonymous TFA Force"""
|
"""Test Anonymous TFA Force"""
|
||||||
request = self.factory.get(reverse('passbook_core:overview'))
|
request = self.factory.get(reverse('passbook_core:overview'))
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
response = tfa_force_verify(overview.OverviewView.as_view())(request)
|
response = totp_force_verify(overview.OverviewView.as_view())(request)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
|
@ -0,0 +1,14 @@
|
||||||
|
"""passbook TOTP Urls"""
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from passbook.totp import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.index, name='totp-index'),
|
||||||
|
path('qr/', views.qr_code, name='totp-qr'),
|
||||||
|
path('verify/', views.verify, name='totp-verify'),
|
||||||
|
# path('enable/', views.TFASetupView.as_view(), name='totp-enable'),
|
||||||
|
path('disable/', views.disable, name='totp-disable'),
|
||||||
|
path('user_settings/', views.user_settings, name='totp-user_settings'),
|
||||||
|
]
|
|
@ -1,4 +1,4 @@
|
||||||
"""passbook Mod 2FA Utils"""
|
"""passbook Mod TOTP Utils"""
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
|
@ -1,4 +1,4 @@
|
||||||
"""passbook 2FA Views"""
|
"""passbook TOTP Views"""
|
||||||
# from base64 import b32encode
|
# from base64 import b32encode
|
||||||
# from binascii import unhexlify
|
# from binascii import unhexlify
|
||||||
|
|
||||||
|
@ -19,8 +19,8 @@ from qrcode.image.svg import SvgPathImage
|
||||||
from passbook.lib.decorators import reauth_required
|
from passbook.lib.decorators import reauth_required
|
||||||
# from passbook.core.models import Event
|
# from passbook.core.models import Event
|
||||||
# from passbook.core.views.wizards import BaseWizardView
|
# from passbook.core.views.wizards import BaseWizardView
|
||||||
from passbook.tfa.forms import TFAVerifyForm
|
from passbook.totp.forms import TOTPVerifyForm
|
||||||
from passbook.tfa.utils import otpauth_url
|
from passbook.totp.utils import otpauth_url
|
||||||
|
|
||||||
TFA_SESSION_KEY = 'passbook_2fa_key'
|
TFA_SESSION_KEY = 'passbook_2fa_key'
|
||||||
|
|
||||||
|
@ -30,22 +30,22 @@ TFA_SESSION_KEY = 'passbook_2fa_key'
|
||||||
def index(request: HttpRequest) -> HttpResponse:
|
def index(request: HttpRequest) -> HttpResponse:
|
||||||
"""Show empty index page"""
|
"""Show empty index page"""
|
||||||
return render(request, 'core/generic.html', {
|
return render(request, 'core/generic.html', {
|
||||||
'text': 'Test 2FA passed'
|
'text': 'Test TOTP passed'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def verify(request: HttpRequest) -> HttpResponse:
|
def verify(request: HttpRequest) -> HttpResponse:
|
||||||
"""Verify 2FA Token"""
|
"""Verify TOTP Token"""
|
||||||
if not user_has_device(request.user):
|
if not user_has_device(request.user):
|
||||||
messages.error(request, _("You don't have 2-Factor Authentication set up."))
|
messages.error(request, _("You don't have 2-Factor Authentication set up."))
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = TFAVerifyForm(request.POST)
|
form = TOTPVerifyForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
device = match_token(request.user, form.cleaned_data.get('code'))
|
device = match_token(request.user, form.cleaned_data.get('code'))
|
||||||
if device:
|
if device:
|
||||||
login(request, device)
|
login(request, device)
|
||||||
messages.success(request, _('Successfully validated 2FA Token.'))
|
messages.success(request, _('Successfully validated TOTP Token.'))
|
||||||
# Check if there is a next GET parameter and redirect to that
|
# Check if there is a next GET parameter and redirect to that
|
||||||
if 'next' in request.GET:
|
if 'next' in request.GET:
|
||||||
return redirect(request.GET.get('next'))
|
return redirect(request.GET.get('next'))
|
||||||
|
@ -53,7 +53,7 @@ def verify(request: HttpRequest) -> HttpResponse:
|
||||||
return redirect(reverse('common-index'))
|
return redirect(reverse('common-index'))
|
||||||
messages.error(request, _('Invalid 2-Factor Token.'))
|
messages.error(request, _('Invalid 2-Factor Token.'))
|
||||||
else:
|
else:
|
||||||
form = TFAVerifyForm()
|
form = TOTPVerifyForm()
|
||||||
|
|
||||||
return render(request, 'generic/form_login.html', {
|
return render(request, 'generic/form_login.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
|
@ -67,13 +67,13 @@ def verify(request: HttpRequest) -> HttpResponse:
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def user_settings(request: HttpRequest) -> HttpResponse:
|
def user_settings(request: HttpRequest) -> HttpResponse:
|
||||||
"""View for user settings to control 2FA"""
|
"""View for user settings to control TOTP"""
|
||||||
static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
|
static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
|
||||||
static_tokens = StaticToken.objects.filter(device=static).order_by('token')
|
static_tokens = StaticToken.objects.filter(device=static).order_by('token')
|
||||||
finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True)
|
finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True)
|
||||||
finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True)
|
finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True)
|
||||||
state = finished_totp_devices.exists() and finished_static_devices.exists()
|
state = finished_totp_devices.exists() and finished_static_devices.exists()
|
||||||
return render(request, 'tfa/user_settings.html', {
|
return render(request, 'totp/user_settings.html', {
|
||||||
'static_tokens': static_tokens,
|
'static_tokens': static_tokens,
|
||||||
'state': state,
|
'state': state,
|
||||||
})
|
})
|
||||||
|
@ -83,7 +83,7 @@ def user_settings(request: HttpRequest) -> HttpResponse:
|
||||||
@reauth_required
|
@reauth_required
|
||||||
@otp_required
|
@otp_required
|
||||||
def disable(request: HttpRequest) -> HttpResponse:
|
def disable(request: HttpRequest) -> HttpResponse:
|
||||||
"""Disable 2FA for user"""
|
"""Disable TOTP for user"""
|
||||||
# Delete all the devices for user
|
# Delete all the devices for user
|
||||||
static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
|
static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
|
||||||
static_tokens = StaticToken.objects.filter(device=static).order_by('token')
|
static_tokens = StaticToken.objects.filter(device=static).order_by('token')
|
||||||
|
@ -92,11 +92,11 @@ def disable(request: HttpRequest) -> HttpResponse:
|
||||||
totp.delete()
|
totp.delete()
|
||||||
for token in static_tokens:
|
for token in static_tokens:
|
||||||
token.delete()
|
token.delete()
|
||||||
messages.success(request, 'Successfully disabled 2FA')
|
messages.success(request, 'Successfully disabled TOTP')
|
||||||
# Create event with email notification
|
# Create event with email notification
|
||||||
# Event.create(
|
# Event.create(
|
||||||
# user=request.user,
|
# user=request.user,
|
||||||
# message=_('You disabled 2FA.'),
|
# message=_('You disabled TOTP.'),
|
||||||
# current=True,
|
# current=True,
|
||||||
# request=request,
|
# request=request,
|
||||||
# send_notification=True)
|
# send_notification=True)
|
||||||
|
@ -108,7 +108,7 @@ def disable(request: HttpRequest) -> HttpResponse:
|
||||||
# class TFASetupView(BaseWizardView):
|
# class TFASetupView(BaseWizardView):
|
||||||
# """Wizard to create a Mail Account"""
|
# """Wizard to create a Mail Account"""
|
||||||
|
|
||||||
# title = _('Set up 2FA')
|
# title = _('Set up TOTP')
|
||||||
# form_list = [TFASetupInitForm, TFASetupStaticForm]
|
# form_list = [TFASetupInitForm, TFASetupStaticForm]
|
||||||
|
|
||||||
# totp_device = None
|
# totp_device = None
|
||||||
|
@ -117,15 +117,15 @@ def disable(request: HttpRequest) -> HttpResponse:
|
||||||
|
|
||||||
# def get_template_names(self):
|
# def get_template_names(self):
|
||||||
# if self.steps.current == '1':
|
# if self.steps.current == '1':
|
||||||
# return 'tfa/wizard_setup_static.html'
|
# return 'totp/wizard_setup_static.html'
|
||||||
# return self.template_name
|
# return self.template_name
|
||||||
|
|
||||||
# def handle_request(self, request: HttpRequest):
|
# def handle_request(self, request: HttpRequest):
|
||||||
# # Check if user has 2FA setup already
|
# # Check if user has TOTP setup already
|
||||||
# finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True)
|
# finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True)
|
||||||
# finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True)
|
# finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True)
|
||||||
# if finished_totp_devices.exists() or finished_static_devices.exists():
|
# if finished_totp_devices.exists() or finished_static_devices.exists():
|
||||||
# messages.error(request, _('You already have 2FA enabled!'))
|
# messages.error(request, _('You already have TOTP enabled!'))
|
||||||
# return redirect(reverse('common-index'))
|
# return redirect(reverse('common-index'))
|
||||||
# # Check if there's an unconfirmed device left to set up
|
# # Check if there's an unconfirmed device left to set up
|
||||||
# totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False)
|
# totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False)
|
||||||
|
@ -182,7 +182,7 @@ def disable(request: HttpRequest) -> HttpResponse:
|
||||||
# # Create event with email notification
|
# # Create event with email notification
|
||||||
# Event.create(
|
# Event.create(
|
||||||
# user=self.request.user,
|
# user=self.request.user,
|
||||||
# message=_('You activated 2FA.'),
|
# message=_('You activated TOTP.'),
|
||||||
# current=True,
|
# current=True,
|
||||||
# request=self.request,
|
# request=self.request,
|
||||||
# send_notification=True)
|
# send_notification=True)
|
Reference in New Issue