user login via otp
This commit is contained in:
parent
6e6f83fad0
commit
7041dfe511
13 changed files with 432 additions and 22 deletions
137
apps/users/backends.py
Normal file
137
apps/users/backends.py
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
|
||||||
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
class OTPBackend(ModelBackend):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Authenticates against settings.AUTH_USER_MODEL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request, phone_number=None, otp=None, **kwargs):
|
||||||
|
if phone_number is None or otp is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = UserModel.objects.get(phone_number=phone_number)
|
||||||
|
# user = UserModel._default_manager.get_by_natural_key(phone_number)
|
||||||
|
except UserModel.DoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
if user.check_otp(otp) and self.user_can_authenticate(user):
|
||||||
|
return user
|
||||||
|
|
||||||
|
def user_can_authenticate(self, user):
|
||||||
|
"""
|
||||||
|
Reject users with is_active=False. Custom user models that don't have
|
||||||
|
that attribute are allowed.
|
||||||
|
"""
|
||||||
|
is_active = getattr(user, 'is_active', None)
|
||||||
|
return is_active or is_active is None
|
||||||
|
|
||||||
|
# def _get_user_permissions(self, user_obj):
|
||||||
|
# return user_obj.user_permissions.all()
|
||||||
|
#
|
||||||
|
# def _get_group_permissions(self, user_obj):
|
||||||
|
# user_groups_field = get_user_model()._meta.get_field('groups')
|
||||||
|
# user_groups_query = 'group__%s' % user_groups_field.related_query_name()
|
||||||
|
# return Permission.objects.filter(**{user_groups_query: user_obj})
|
||||||
|
#
|
||||||
|
# def _get_permissions(self, user_obj, obj, from_name):
|
||||||
|
# """
|
||||||
|
# Return the permissions of `user_obj` from `from_name`. `from_name` can
|
||||||
|
# be either "group" or "user" to return permissions from
|
||||||
|
# `_get_group_permissions` or `_get_user_permissions` respectively.
|
||||||
|
# """
|
||||||
|
# if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
|
||||||
|
# return set()
|
||||||
|
#
|
||||||
|
# perm_cache_name = '_%s_perm_cache' % from_name
|
||||||
|
# if not hasattr(user_obj, perm_cache_name):
|
||||||
|
# if user_obj.is_superuser:
|
||||||
|
# perms = Permission.objects.all()
|
||||||
|
# else:
|
||||||
|
# perms = getattr(self, '_get_%s_permissions' % from_name)(user_obj)
|
||||||
|
# perms = perms.values_list('content_type__app_label', 'codename').order_by()
|
||||||
|
# setattr(user_obj, perm_cache_name, {"%s.%s" % (ct, name) for ct, name in perms})
|
||||||
|
# return getattr(user_obj, perm_cache_name)
|
||||||
|
#
|
||||||
|
# def get_user_permissions(self, user_obj, obj=None):
|
||||||
|
# """
|
||||||
|
# Return a set of permission strings the user `user_obj` has from their
|
||||||
|
# `user_permissions`.
|
||||||
|
# """
|
||||||
|
# return self._get_permissions(user_obj, obj, 'user')
|
||||||
|
#
|
||||||
|
# def get_group_permissions(self, user_obj, obj=None):
|
||||||
|
# """
|
||||||
|
# Return a set of permission strings the user `user_obj` has from the
|
||||||
|
# groups they belong.
|
||||||
|
# """
|
||||||
|
# return self._get_permissions(user_obj, obj, 'group')
|
||||||
|
#
|
||||||
|
# def get_all_permissions(self, user_obj, obj=None):
|
||||||
|
# if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
|
||||||
|
# return set()
|
||||||
|
# if not hasattr(user_obj, '_perm_cache'):
|
||||||
|
# user_obj._perm_cache = super().get_all_permissions(user_obj)
|
||||||
|
# return user_obj._perm_cache
|
||||||
|
#
|
||||||
|
# def has_perm(self, user_obj, perm, obj=None):
|
||||||
|
# return user_obj.is_active and super().has_perm(user_obj, perm, obj=obj)
|
||||||
|
#
|
||||||
|
# def has_module_perms(self, user_obj, app_label):
|
||||||
|
# """
|
||||||
|
# Return True if user_obj has any permissions in the given app_label.
|
||||||
|
# """
|
||||||
|
# return user_obj.is_active and any(
|
||||||
|
# perm[:perm.index('.')] == app_label
|
||||||
|
# for perm in self.get_all_permissions(user_obj)
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# def with_perm(self, perm, is_active=True, include_superusers=True, obj=None):
|
||||||
|
# """
|
||||||
|
# Return users that have permission "perm". By default, filter out
|
||||||
|
# inactive users and include superusers.
|
||||||
|
# """
|
||||||
|
# if isinstance(perm, str):
|
||||||
|
# try:
|
||||||
|
# app_label, codename = perm.split('.')
|
||||||
|
# except ValueError:
|
||||||
|
# raise ValueError(
|
||||||
|
# 'Permission name should be in the form '
|
||||||
|
# 'app_label.permission_codename.'
|
||||||
|
# )
|
||||||
|
# elif not isinstance(perm, Permission):
|
||||||
|
# raise TypeError(
|
||||||
|
# 'The `perm` argument must be a string or a permission instance.'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# UserModel = get_user_model()
|
||||||
|
# if obj is not None:
|
||||||
|
# return UserModel._default_manager.none()
|
||||||
|
#
|
||||||
|
# permission_q = Q(group__user=OuterRef('pk')) | Q(user=OuterRef('pk'))
|
||||||
|
# if isinstance(perm, Permission):
|
||||||
|
# permission_q &= Q(pk=perm.pk)
|
||||||
|
# else:
|
||||||
|
# permission_q &= Q(codename=codename, content_type__app_label=app_label)
|
||||||
|
#
|
||||||
|
# user_q = Exists(Permission.objects.filter(permission_q))
|
||||||
|
# if include_superusers:
|
||||||
|
# user_q |= Q(is_superuser=True)
|
||||||
|
# if is_active is not None:
|
||||||
|
# user_q &= Q(is_active=is_active)
|
||||||
|
#
|
||||||
|
# return UserModel._default_manager.filter(user_q)
|
||||||
|
#
|
||||||
|
# def get_user(self, user_id):
|
||||||
|
# try:
|
||||||
|
# user = UserModel._default_manager.get(pk=user_id)
|
||||||
|
# except UserModel.DoesNotExist:
|
||||||
|
# return None
|
||||||
|
# return user if self.user_can_authenticate(user) else None
|
||||||
|
#
|
||||||
|
#
|
||||||
84
apps/users/forms.py
Normal file
84
apps/users/forms.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth import (authenticate, get_user_model)
|
||||||
|
from django.contrib.auth.forms import UsernameField
|
||||||
|
from django.utils.text import capfirst
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
class OTPAuthenticationForm(forms.Form):
|
||||||
|
"""
|
||||||
|
Base class for authenticating users. Extend this to get a form that accepts
|
||||||
|
username/password logins.
|
||||||
|
"""
|
||||||
|
phone_number = UsernameField(widget=forms.TextInput(attrs={'autofocus': True}))
|
||||||
|
otp = forms.CharField(
|
||||||
|
label=_("otp"),
|
||||||
|
strip=False,
|
||||||
|
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
|
||||||
|
)
|
||||||
|
|
||||||
|
error_messages = {
|
||||||
|
'invalid_login': _(
|
||||||
|
"Please enter a correct %(phone_number)s and otp. Note that both "
|
||||||
|
"fields may be case-sensitive."
|
||||||
|
),
|
||||||
|
'inactive': _("This account is inactive."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, request=None, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
The 'request' parameter is set for custom auth use by subclasses.
|
||||||
|
The form data comes in via the standard 'data' kwarg.
|
||||||
|
"""
|
||||||
|
self.request = request
|
||||||
|
self.user_cache = None
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Set the max length and label for the "username" field.
|
||||||
|
self.phone_number_field = UserModel._meta.get_field('phone_number')
|
||||||
|
phone_number_max_length = self.phone_number_field.max_length or 24
|
||||||
|
self.fields['phone_number'].max_length = phone_number_max_length
|
||||||
|
self.fields['phone_number'].widget.attrs['maxlength'] = phone_number_max_length
|
||||||
|
if self.fields['phone_number'].label is None:
|
||||||
|
self.fields['phone_number'].label = capfirst(self.phone_number_field.verbose_name)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
phone_number = self.cleaned_data.get('phone_number')
|
||||||
|
otp = self.cleaned_data.get('otp')
|
||||||
|
|
||||||
|
if phone_number is not None and otp:
|
||||||
|
self.user_cache = authenticate(self.request, phone_number=phone_number, otp=otp)
|
||||||
|
if self.user_cache is None:
|
||||||
|
raise self.get_invalid_login_error()
|
||||||
|
else:
|
||||||
|
self.confirm_login_allowed(self.user_cache)
|
||||||
|
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
def confirm_login_allowed(self, user):
|
||||||
|
"""
|
||||||
|
Controls whether the given User may log in. This is a policy setting,
|
||||||
|
independent of end-user authentication. This default behavior is to
|
||||||
|
allow login by active users, and reject login by inactive users.
|
||||||
|
|
||||||
|
If the given user cannot log in, this method should raise a
|
||||||
|
``forms.ValidationError``.
|
||||||
|
|
||||||
|
If the given user may log in, this method should return None.
|
||||||
|
"""
|
||||||
|
if not user.is_active:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
self.error_messages['inactive'],
|
||||||
|
code='inactive',
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_user(self):
|
||||||
|
return self.user_cache
|
||||||
|
|
||||||
|
def get_invalid_login_error(self):
|
||||||
|
return forms.ValidationError(
|
||||||
|
self.error_messages['invalid_login'],
|
||||||
|
code='invalid_login',
|
||||||
|
params={'phone_number': self.phone_number_field.verbose_name},
|
||||||
|
)
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.views import LoginView
|
||||||
|
from django.shortcuts import render
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from oauth2_provider.contrib.rest_framework import TokenHasReadWriteScope, TokenHasScope, IsAuthenticatedOrTokenHasScope
|
from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
|
||||||
from rest_framework import generics, permissions, status
|
from rest_framework import generics, permissions, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from apps.users.forms import OTPAuthenticationForm
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, ChangePasswordSerializer
|
from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, \
|
||||||
|
ChangePasswordSerializer
|
||||||
|
|
||||||
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UserListView(generics.ListAPIView):
|
class UserListView(generics.ListAPIView):
|
||||||
|
|
@ -67,3 +76,15 @@ class ChangePasswordView(generics.UpdateAPIView):
|
||||||
return Response({"state": 'success'}, status=status.HTTP_200_OK)
|
return Response({"state": 'success'}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def home(request):
|
||||||
|
return render(request, 'registration/profile.html')
|
||||||
|
|
||||||
|
|
||||||
|
class OTPLoginView(LoginView):
|
||||||
|
"""
|
||||||
|
Display the login form and handle the login action.
|
||||||
|
"""
|
||||||
|
form_class = OTPAuthenticationForm
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,11 @@ TEMPLATES = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'apps.users.backends.OTPBackend',
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
)
|
||||||
|
|
||||||
WSGI_APPLICATION = 'gooyal_accounts.wsgi.application'
|
WSGI_APPLICATION = 'gooyal_accounts.wsgi.application'
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
|
@ -144,6 +149,11 @@ USE_TZ = True
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
|
|
||||||
|
# LOGIN_URL = '/accounts/login/'
|
||||||
|
# LOGIN_REDIRECT_URL = '/accounts/profile/'
|
||||||
|
|
||||||
|
LOGIN_URL = '/login/'
|
||||||
|
LOGIN_REDIRECT_URL = '/'
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'users.User'
|
AUTH_USER_MODEL = 'users.User'
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,23 @@ Including another URLconf
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib.auth.views import LogoutView
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from apps.transactions.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt
|
from apps.transactions.views import TransactionList, TransactionDetail, TransactionPay, TransactionReceipt
|
||||||
from apps.users.views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView
|
from apps.users.views import UserListView, UserDetailView, AccountView, RequestOTPView, ChangePasswordView, \
|
||||||
|
OTPLoginView, home
|
||||||
from django.contrib.auth import urls as auth_urls
|
from django.contrib.auth import urls as auth_urls
|
||||||
|
|
||||||
# Setup the URLs and include login URLs for the browsable API.
|
# Setup the URLs and include login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('accounts/', include(auth_urls)),
|
# path('accounts/', include(auth_urls)),
|
||||||
|
path('login/', OTPLoginView.as_view(), name='login'),
|
||||||
|
path('logout/', LogoutView.as_view(), name='logout'),
|
||||||
|
path('', home, name='home'),
|
||||||
|
|
||||||
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
|
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
|
||||||
|
|
||||||
path('users/', UserListView.as_view()),
|
path('users/', UserListView.as_view()),
|
||||||
|
|
|
||||||
57
templates/base.html
Normal file
57
templates/base.html
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
{% with site_title="Gooyal Accounts" %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
{% load static %}
|
||||||
|
{# {% load jalali_tags %}#}
|
||||||
|
{% load i18n %}
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<title>{{ site_title }}</title>
|
||||||
|
<meta name="description" content="{{ site_description }}">
|
||||||
|
<meta name="revisit-after" content="1 hour">
|
||||||
|
<META NAME="ROBOTS" CONTENT="INDEX, FOLLOW">
|
||||||
|
<meta name="keywords" content="{{ _('site_keywords') }}">
|
||||||
|
|
||||||
|
{# <link rel="icon" href="{% static "logo.png" %}" sizes="32x32" type="image/png">#}
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
|
||||||
|
crossorigin="anonymous">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="font-vazir" style="direction: rtl; text-align: right">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
<a class="navbar-brand" href="#">Toomar</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
|
||||||
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a class="nav-link" href="{% url 'home' %}">home <span class="sr-only"></span></a>
|
||||||
|
</li>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'logout' %}" >({{ request.user.phone_number }}) Logout</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="font-vazir container mb-3 mt-3">
|
||||||
|
{% include "include/message_frame.html" %}
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
|
||||||
|
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
|
||||||
|
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
|
||||||
|
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{% endwith %}
|
||||||
|
|
@ -1,12 +1,4 @@
|
||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Title</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% block content %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
</body>
|
{% block content %}
|
||||||
</html>
|
{% endblock %}
|
||||||
36
templates/include/field_frame.html
Normal file
36
templates/include/field_frame.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
{# feild #}
|
||||||
|
{# form #}
|
||||||
|
{# placeholder #}
|
||||||
|
|
||||||
|
{% with field_class=field_class|default_if_none:'' %}
|
||||||
|
<div class="form-group">
|
||||||
|
{{ field.label_tag }}
|
||||||
|
{% if form.is_bound %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="input-group">
|
||||||
|
{% render_field field|add_class:field_class class="form-control is-invalid" placeholder=placeholder %}
|
||||||
|
</div>
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="input-group">
|
||||||
|
{% render_field field|add_class:field_class class="form-control is-valid" placeholder=placeholder %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="input-group">
|
||||||
|
{% render_field field|add_class:field_class class="form-control" placeholder=placeholder %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if field.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ field.help_text | safe }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
38
templates/include/form_frame.html
Normal file
38
templates/include/form_frame.html
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
{% for hidden_field in form.hidden_fields %}
|
||||||
|
{{ hidden_field }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
<div class="form-group">
|
||||||
|
{{ field.label_tag }}
|
||||||
|
|
||||||
|
{% if form.is_bound %}
|
||||||
|
{% if field.errors %}
|
||||||
|
{% render_field field class="form-control is-invalid" %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% render_field field class="form-control is-valid" %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% render_field field class="form-control" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if field.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ field.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
16
templates/include/message_frame.html
Normal file
16
templates/include/message_frame.html
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% if messages %}
|
||||||
|
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert{% if message.tags %} alert-{{ message.tags }}{% endif %} alert-dismissible fade show"
|
||||||
|
role="alert">
|
||||||
|
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} {{ _('important') }} : {% endif %}
|
||||||
|
{{ message }}
|
||||||
|
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
6
templates/registration/logged_out.html
Normal file
6
templates/registration/logged_out.html
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base_generic.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
success log out
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<p>Your username and password didn't match. Please try again.</p>
|
<p>Your phone number and otp didn't match. Please try again.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if next %}
|
{% if next %}
|
||||||
|
|
@ -20,13 +20,13 @@
|
||||||
<table>
|
<table>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ form.username.label_tag }}</td>
|
<td>{{ form.phone_number.label_tag }}</td>
|
||||||
<td>{{ form.username }}</td>
|
<td>{{ form.phone_number }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ form.password.label_tag }}</td>
|
<td>{{ form.otp.label_tag }}</td>
|
||||||
<td>{{ form.password }}</td>
|
<td>{{ form.otp }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
@ -34,7 +34,5 @@
|
||||||
<input type="hidden" name="next" value="{{ next }}" />
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{# Assumes you setup the password_reset view in your URLconf #}
|
|
||||||
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
9
templates/registration/profile.html
Normal file
9
templates/registration/profile.html
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base_generic.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h2>Homepage</h2>
|
||||||
|
<p>Hello {{ user.username }}!</p>
|
||||||
|
<p>your phone number {{ user.phone_number }}!</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Loading…
Add table
Reference in a new issue