Add: account views, tests, override user model

This commit is contained in:
Stepan Zhukovsky 2023-09-10 12:34:54 +09:00
parent 305001c9ab
commit 2cba6321c2
25 changed files with 375 additions and 19 deletions

View File

@ -1,3 +1,6 @@
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)

View 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'),
)

View 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()),
],
),
]

View File

@ -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')

View 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 %}

View File

@ -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="&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;"
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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View 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 %}

View File

@ -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 %}

View 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 %}

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

View 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)

View File

@ -1,6 +1,10 @@
from django.conf import settings
from django.urls import path
from django.contrib.auth.views import LogoutView
from django.urls import path, reverse_lazy
from django.contrib.auth.views import (
LogoutView,
PasswordChangeView,
PasswordChangeDoneView
)
from rest_framework_simplejwt.views import (
TokenObtainPairView,
@ -8,6 +12,8 @@ from rest_framework_simplejwt.views import (
TokenVerifyView
)
from . import views
app_name = 'account'
@ -17,7 +23,35 @@ urlpatterns = [
'account/logout/',
LogoutView.as_view(next_page=settings.LOGOUT_REDIRECT_URL),
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 += [

View File

@ -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")}'

View File

@ -1,12 +1,13 @@
from pathlib import Path
from django.core.files.base import ContentFile
from django.conf import settings
from django.contrib.auth.models import User
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User
from collector.models import Archive, Platform, Ticket

View File

@ -2,10 +2,11 @@ import uuid
import hashlib
from django.core.validators import MaxValueValidator, MinValueValidator
from django.contrib.auth.models import User
from django.db import models
from django.urls import reverse
from account.models import User
from .utils.helpers import logs_dir_path

View File

@ -1,9 +1,9 @@
from pathlib import Path
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.files.base import ContentFile
from django.conf import settings
from account.models import User
from collector.models import Platform, Ticket, Archive

View File

@ -1,6 +1,7 @@
from django.test import TestCase
from django.urls import resolve, reverse
from django.contrib.auth.models import User
from account.models import User
from collector import views
from collector.models import Ticket, Platform

View File

@ -1,6 +1,7 @@
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from account.models import User
from collector.models import Ticket, Platform

View File

@ -252,3 +252,6 @@ SIMPLE_JWT = {
LOGIN_URL = 'two_factor:login'
LOGIN_REDIRECT_URL = 'collector:index'
LOGOUT_REDIRECT_URL = 'two_factor:login'
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-user-model
AUTH_USER_MODEL = 'account.User'

View File

@ -1,9 +1,13 @@
{% if request.user.is_authenticated %}
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary">
<i class="bi bi-person-square"></i> {{ request.user }}
</button>
<a
type="button"
class="btn btn-outline-secondary"
href="{% url 'account:show_profile' %}"
>
<i class="bi bi-person-circle"></i> {{ request.user }}
</a>
<button
type="button"
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
@ -24,7 +28,7 @@
href="{% url 'two_factor:profile' %}"
class="dropdown-item"
type="button">
<i class="bi bi-gear"></i> Settings
<i class="bi bi-dice-5"></i> 2FA
</a>
</li>
<li><hr class="dropdown-divider" /></li>

View File

@ -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">
<!--Brand logo -->
{% include 'includes/brand.html' %}