Add: account views, tests, override user model

This commit is contained in:
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")}'