Add: account views, tests, override user model
This commit is contained in:
parent
305001c9ab
commit
2cba6321c2
@ -1,3 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from .models import User
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
admin.site.register(User, UserAdmin)
|
||||||
|
37
logs_collector/account/forms.py
Normal file
37
logs_collector/account/forms.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from crispy_forms.helper import FormHelper
|
||||||
|
from crispy_forms.layout import Layout, Submit, Div
|
||||||
|
from crispy_forms.bootstrap import PrependedText
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
'email',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(UserProfileForm, self).__init__(*args, **kwargs)
|
||||||
|
self.helper = FormHelper(self)
|
||||||
|
self.helper.form_show_labels = False
|
||||||
|
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
Div(
|
||||||
|
PrependedText(
|
||||||
|
'email',
|
||||||
|
mark_safe('<i class="bi bi-envelope-at"></i>'),
|
||||||
|
placeholder="email"
|
||||||
|
),
|
||||||
|
PrependedText('first_name', 'First name:'),
|
||||||
|
PrependedText('last_name', 'Last name:'),
|
||||||
|
css_class='col-lg-6'
|
||||||
|
),
|
||||||
|
Submit('submit', 'Save', css_class='btn btn-primary'),
|
||||||
|
)
|
44
logs_collector/account/migrations/0001_initial.py
Normal file
44
logs_collector/account/migrations/0001_initial.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 4.2 on 2023-09-08 12:27
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,10 @@
|
|||||||
from django.db import models
|
from django.urls import reverse
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
# using-a-custom-user-model-when-starting-a-project
|
||||||
|
# https://docs.djangoproject.com/en/4.2/topics/auth/customizing/
|
||||||
|
class User(AbstractUser):
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('account:show_profile')
|
||||||
|
30
logs_collector/account/templates/account/base.html
Normal file
30
logs_collector/account/templates/account/base.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block account_head %}
|
||||||
|
<title>{% block title %}{% endblock title %}</title>
|
||||||
|
{% endblock account_head %}
|
||||||
|
|
||||||
|
{% block account_content %}
|
||||||
|
<header class="sticky-top">
|
||||||
|
<section>
|
||||||
|
{% include 'includes/navigation.html' %}
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section>
|
||||||
|
{% block main %}{% endblock main %}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="footer mt-auto">
|
||||||
|
<section>
|
||||||
|
{% include 'includes/footer.html' %}
|
||||||
|
</section>
|
||||||
|
</footer>
|
||||||
|
{% endblock account_content %}
|
||||||
|
|
||||||
|
{% block account_scripts %}
|
||||||
|
<script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script>
|
||||||
|
{% block bs %}{% endblock bs %}
|
||||||
|
{% block jquery %}{% endblock jquery %}
|
||||||
|
{% endblock account_scripts %}
|
@ -0,0 +1,39 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h5 class="card-title">Authentication</h5>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text"><i class="bi bi-person-circle"></i></span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="{{ request.user.username }}"
|
||||||
|
aria-label="Username"
|
||||||
|
disabled
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text"><i class="bi bi-shield-lock"></i></span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="●●●●●●●●"
|
||||||
|
aria-label="Password"
|
||||||
|
disabled
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
href="{% url 'account:password_change' %}"
|
||||||
|
>
|
||||||
|
<i class="bi bi-pencil-square"></i> Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,26 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h5 class="card-title">Profile</h5>
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text"><i class="bi bi-envelope-at"></i></span>
|
||||||
|
<input type="text" class="form-control" placeholder="{{ request.user.email }}" aria-label="Email" disabled readonly>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">First name:</span>
|
||||||
|
<input type="text" class="form-control" placeholder="{{ request.user.first_name }}" aria-label="Username" disabled readonly>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">Last name:</i></span>
|
||||||
|
<input type="text" class="form-control" placeholder="{{ request.user.last_name }}" aria-label="Email" disabled readonly>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="{% url 'account:update_profile' %}"
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
>
|
||||||
|
<i class="bi bi-pencil-square"></i> Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'account/profile.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% block password_change %}
|
||||||
|
<form method="post">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<p><input class="btn btn-primary" type="submit" value="Change" /></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock password_change %}
|
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'account/profile_info.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% block profile_alerts %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<div class=" d-flex align-items-center mt-1">
|
||||||
|
<h5><i class="bi bi-check-circle-fill"></i> Password changed</h5>
|
||||||
|
</div>
|
||||||
|
Your password has been successfully changed.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endblock profile_alerts %}
|
17
logs_collector/account/templates/account/profile.html
Normal file
17
logs_collector/account/templates/account/profile.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'account/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% block title %} {{ title }} {% endblock title %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="container mt-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Account:</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% block profile_info %}{% endblock profile_info %}
|
||||||
|
{% block profile_update %}{% endblock profile_update %}
|
||||||
|
{% block password_change %}{% endblock password_change %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock main %}
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'account/profile.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% block profile_info %}
|
||||||
|
{% block profile_alerts %}{% endblock profile_alerts %}
|
||||||
|
{% include 'account/includes/auth_credentials.html' %}
|
||||||
|
{% include 'account/includes/profile_credentials.html' %}
|
||||||
|
{% endblock profile_info %}
|
13
logs_collector/account/templates/account/profile_update.html
Normal file
13
logs_collector/account/templates/account/profile_update.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'account/profile.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% block profile_update %}
|
||||||
|
{% include 'account/includes/auth_credentials.html' %}
|
||||||
|
<div class="container">
|
||||||
|
<h5 class="card-title">Profile</h5>
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
{% crispy form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock profile_update %}
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
0
logs_collector/account/tests/__init__.py
Normal file
0
logs_collector/account/tests/__init__.py
Normal file
36
logs_collector/account/tests/test_urls.py
Normal file
36
logs_collector/account/tests/test_urls.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import resolve, reverse
|
||||||
|
from django.contrib.auth.views import (
|
||||||
|
LogoutView,
|
||||||
|
PasswordChangeView,
|
||||||
|
PasswordChangeDoneView
|
||||||
|
)
|
||||||
|
|
||||||
|
from account import views
|
||||||
|
|
||||||
|
|
||||||
|
class TestUrls(TestCase):
|
||||||
|
|
||||||
|
# READ:
|
||||||
|
def test_account_logout_url_is_resolved(self):
|
||||||
|
url = reverse('account:logout')
|
||||||
|
self.assertEquals(resolve(url).func.view_class, LogoutView)
|
||||||
|
|
||||||
|
def test_account_show_url_is_resolved(self):
|
||||||
|
url = reverse('account:show_profile')
|
||||||
|
self.assertEquals(resolve(url).func.view_class, views.DetailProfile)
|
||||||
|
|
||||||
|
def test_password_change_done_url_is_resolved(self):
|
||||||
|
url = reverse('account:password_change_done')
|
||||||
|
self.assertEquals(
|
||||||
|
resolve(url).func.view_class, PasswordChangeDoneView
|
||||||
|
)
|
||||||
|
|
||||||
|
# UPDATE:
|
||||||
|
def test_password_change_url_is_resolved(self):
|
||||||
|
url = reverse('account:password_change')
|
||||||
|
self.assertEquals(resolve(url).func.view_class, PasswordChangeView)
|
||||||
|
|
||||||
|
def test_account_update_url_is_resolved(self):
|
||||||
|
url = reverse('account:update_profile')
|
||||||
|
self.assertEquals(resolve(url).func.view_class, views.UpdateProfile)
|
@ -1,6 +1,10 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import path
|
from django.urls import path, reverse_lazy
|
||||||
from django.contrib.auth.views import LogoutView
|
from django.contrib.auth.views import (
|
||||||
|
LogoutView,
|
||||||
|
PasswordChangeView,
|
||||||
|
PasswordChangeDoneView
|
||||||
|
)
|
||||||
|
|
||||||
from rest_framework_simplejwt.views import (
|
from rest_framework_simplejwt.views import (
|
||||||
TokenObtainPairView,
|
TokenObtainPairView,
|
||||||
@ -8,6 +12,8 @@ from rest_framework_simplejwt.views import (
|
|||||||
TokenVerifyView
|
TokenVerifyView
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
app_name = 'account'
|
app_name = 'account'
|
||||||
|
|
||||||
@ -17,7 +23,35 @@ urlpatterns = [
|
|||||||
'account/logout/',
|
'account/logout/',
|
||||||
LogoutView.as_view(next_page=settings.LOGOUT_REDIRECT_URL),
|
LogoutView.as_view(next_page=settings.LOGOUT_REDIRECT_URL),
|
||||||
name='logout'
|
name='logout'
|
||||||
)
|
),
|
||||||
|
# CHANGE PASSWORD:
|
||||||
|
path(
|
||||||
|
'account/password-change/',
|
||||||
|
PasswordChangeView.as_view(
|
||||||
|
template_name='account/password_change.html',
|
||||||
|
success_url=reverse_lazy('account:password_change_done'),
|
||||||
|
),
|
||||||
|
name='password_change'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'account/password-change/done/',
|
||||||
|
PasswordChangeDoneView.as_view(
|
||||||
|
template_name='account/password_change_done.html'
|
||||||
|
),
|
||||||
|
name='password_change_done'
|
||||||
|
),
|
||||||
|
# UPDATE:
|
||||||
|
path(
|
||||||
|
'account/update/',
|
||||||
|
views.UpdateProfile.as_view(),
|
||||||
|
name='update_profile'
|
||||||
|
),
|
||||||
|
# READ:
|
||||||
|
path(
|
||||||
|
'account/show/',
|
||||||
|
views.DetailProfile.as_view(),
|
||||||
|
name='show_profile'
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.views import generic
|
||||||
|
|
||||||
|
from collector.utils.mixins import ExtraContextMixin
|
||||||
|
|
||||||
|
from .forms import UserProfileForm
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class DetailProfile(LoginRequiredMixin, ExtraContextMixin, generic.DetailView):
|
||||||
|
model = User
|
||||||
|
template_name = 'account/profile_info.html'
|
||||||
|
context_object_name = 'profile'
|
||||||
|
|
||||||
|
def get_title(self, **kwargs):
|
||||||
|
return f'{self.title} - {self.request.user}'
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.model.objects.get(username=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateProfile(LoginRequiredMixin, ExtraContextMixin, generic.UpdateView):
|
||||||
|
model = User
|
||||||
|
template_name = 'account/profile_update.html'
|
||||||
|
context_object_name = 'profile'
|
||||||
|
form_class = UserProfileForm
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.model.objects.get(username=self.request.user)
|
||||||
|
|
||||||
|
def get_title(self, **kwargs):
|
||||||
|
return f'{self.title} - {self.kwargs.get("username", "account")}'
|
@ -1,12 +1,13 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
|
|
||||||
from collector.models import Archive, Platform, Ticket
|
from collector.models import Archive, Platform, Ticket
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,10 +2,11 @@ import uuid
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
|
|
||||||
from .utils.helpers import logs_dir_path
|
from .utils.helpers import logs_dir_path
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
from collector.models import Platform, Ticket, Archive
|
from collector.models import Platform, Ticket, Archive
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
from account.models import User
|
||||||
|
|
||||||
from collector import views
|
from collector import views
|
||||||
from collector.models import Ticket, Platform
|
from collector.models import Ticket, Platform
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
from account.models import User
|
||||||
|
|
||||||
from collector.models import Ticket, Platform
|
from collector.models import Ticket, Platform
|
||||||
|
|
||||||
|
@ -252,3 +252,6 @@ SIMPLE_JWT = {
|
|||||||
LOGIN_URL = 'two_factor:login'
|
LOGIN_URL = 'two_factor:login'
|
||||||
LOGIN_REDIRECT_URL = 'collector:index'
|
LOGIN_REDIRECT_URL = 'collector:index'
|
||||||
LOGOUT_REDIRECT_URL = 'two_factor:login'
|
LOGOUT_REDIRECT_URL = 'two_factor:login'
|
||||||
|
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-user-model
|
||||||
|
AUTH_USER_MODEL = 'account.User'
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="button" class="btn btn-outline-secondary">
|
<a
|
||||||
<i class="bi bi-person-square"></i> {{ request.user }}
|
type="button"
|
||||||
</button>
|
class="btn btn-outline-secondary"
|
||||||
|
href="{% url 'account:show_profile' %}"
|
||||||
|
>
|
||||||
|
<i class="bi bi-person-circle"></i> {{ request.user }}
|
||||||
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
|
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
|
||||||
@ -24,7 +28,7 @@
|
|||||||
href="{% url 'two_factor:profile' %}"
|
href="{% url 'two_factor:profile' %}"
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
type="button">
|
type="button">
|
||||||
<i class="bi bi-gear"></i> Settings
|
<i class="bi bi-dice-5"></i> 2FA
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li><hr class="dropdown-divider" /></li>
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
<nav class="navbar navbar-expand-xl bg-body-tertiary">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!--Brand logo -->
|
<!--Brand logo -->
|
||||||
{% include 'includes/brand.html' %}
|
{% include 'includes/brand.html' %}
|
||||||
|
Loading…
Reference in New Issue
Block a user