Merge branch 'main' into nmb/whomami

This commit is contained in:
Neil Martinsen-Burrell 2022-09-27 13:53:48 -05:00
commit 6dd154206d
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
15 changed files with 465 additions and 17 deletions

View file

@ -2,3 +2,5 @@
max-line-length = 88
max-complexity = 10
extend-ignore = E203
# migrations are auto-generated and often break rules
exclude=registrar/migrations/*

20
src/registrar/admin.py Normal file
View file

@ -0,0 +1,20 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User, UserProfile
class UserProfileInline(admin.StackedInline):
"""Edit a user's profile on the user page."""
model = UserProfile
class MyUserAdmin(UserAdmin):
"""Custom user admin class to use our inlines."""
inlines = [UserProfileInline]
admin.site.register(User, MyUserAdmin)

17
src/registrar/apps.py Normal file
View file

@ -0,0 +1,17 @@
from django.apps import AppConfig
class RegistrarConfig(AppConfig):
"""Configure signal handling for our registrar Django application."""
name = "registrar"
def ready(self):
"""Runs when all Django applications have been loaded.
We use it here to load signals that connect related models.
"""
# noqa here because we are importing something to make the signals
# get registered, but not using what we import
from . import signals # noqa

View file

@ -188,6 +188,9 @@ DATABASES = {
# Specify default field type to use for primary keys
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Use our user model instead of the default
AUTH_USER_MODEL = "registrar.User"
# endregion
# region: Email-------------------------------------------------------------###
@ -569,6 +572,10 @@ if DEBUG:
INSTALLED_APPS += ("nplusone.ext.django",)
MIDDLEWARE += ("nplusone.ext.django.NPlusOneMiddleware",)
NPLUSONE_RAISE = True
NPLUSONE_WHITELIST = [
{"model": "admin.LogEntry", "field": "user"},
{"model": "registrar.UserProfile"},
]
# insert the amazing django-debug-toolbar
INSTALLED_APPS += ("debug_toolbar",)

View file

@ -7,13 +7,14 @@ For more information see:
from django.contrib import admin
from django.urls import include, path
from registrar.views import health, index, whoami
from registrar.views import health, index, profile, whoami
urlpatterns = [
path("", index.index, name="home"),
path("whoami", whoami.whoami, name="whoami"),
path("admin/", admin.site.urls),
path("health/", health.health),
path("edit_profile/", profile.edit_profile, name="edit-profile"),
path("openid/", include("djangooidc.urls")),
# these views respect the DEBUG setting
path("__debug__/", include("debug_toolbar.urls")),

21
src/registrar/forms.py Normal file
View file

@ -0,0 +1,21 @@
from django import forms
from .models import UserProfile
class EditProfileForm(forms.ModelForm):
"""Custom form class for editing a UserProfile.
We can add whatever fields we want to this form and customize how they
are displayed. The form is rendered into a template `profile.html` by a
view called `edit_profile` in `profile.py`.
"""
display_name = forms.CharField(
widget=forms.TextInput(attrs={"class": "usa-input"}), label="Display Name"
)
class Meta:
model = UserProfile
fields = ["display_name"]

View file

@ -0,0 +1,172 @@
# Generated by Django 4.1.1 on 2022-09-26 15:26
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
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()),
],
),
migrations.CreateModel(
name="UserProfile",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("street1", models.TextField(blank=True)),
("street2", models.TextField(blank=True)),
("street3", models.TextField(blank=True)),
("city", models.TextField(blank=True)),
("sp", models.TextField(blank=True)),
("pc", models.TextField(blank=True)),
("cc", models.TextField(blank=True)),
("voice", models.TextField(blank=True)),
("fax", models.TextField(blank=True)),
("email", models.TextField(blank=True)),
("display_name", models.TextField()),
(
"user",
models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
]

View file

View file

@ -0,0 +1,3 @@
from .models import User, UserProfile
__all__ = ["User", "UserProfile"]

View file

@ -0,0 +1,85 @@
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""
A custom user model that performs identically to the default user model
but can be customized later.
"""
def __str__(self):
try:
return self.userprofile.display_name
except ObjectDoesNotExist:
return self.username
class TimeStampedModel(models.Model):
"""
An abstract base model that provides self-updating
`created_at` and `updated_at` fields.
"""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
# don't put anything else here, it will be ignored
class AddressModel(models.Model):
"""
An abstract base model that provides common fields
for postal addresses.
"""
# contact's street (null ok)
street1 = models.TextField(blank=True)
# contact's street (null ok)
street2 = models.TextField(blank=True)
# contact's street (null ok)
street3 = models.TextField(blank=True)
# contact's city
city = models.TextField(blank=True)
# contact's state or province (null ok)
sp = models.TextField(blank=True)
# contact's postal code (null ok)
pc = models.TextField(blank=True)
# contact's country code
cc = models.TextField(blank=True)
class Meta:
abstract = True
# don't put anything else here, it will be ignored
class ContactModel(models.Model):
"""
An abstract base model that provides common fields
for contact information.
"""
voice = models.TextField(blank=True)
fax = models.TextField(blank=True)
email = models.TextField(blank=True)
class Meta:
abstract = True
# don't put anything else here, it will be ignored
class UserProfile(TimeStampedModel, ContactModel, AddressModel):
user = models.OneToOneField(User, null=True, on_delete=models.CASCADE)
display_name = models.TextField()
def __str__(self):
if self.display_name:
return self.display_name
else:
try:
return self.user.username
except ObjectDoesNotExist:
return "No username"

23
src/registrar/signals.py Normal file
View file

@ -0,0 +1,23 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import User, UserProfile
@receiver(post_save, sender=User)
def handle_profile(sender, instance, **kwargs):
"""Method for when a User is saved.
If the user is being created, then create a matching UserProfile. Otherwise
save an updated profile or create one if it doesn't exist.
"""
if kwargs.get("created", False):
UserProfile.objects.create(user=instance)
else:
# the user is not being created.
if hasattr(instance, "userprofile"):
instance.userprofile.save()
else:
UserProfile.objects.create(user=instance)

View file

@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% block title %}
Edit your User Profile
{% endblock title %}
{% block content %}
<form class="usa-form usa-form--large" method="post" enctype="multipart/form-data">
{% csrf_token %}
<fieldset class="usa-fieldset">
<legend class="usa-legend usa-legend--large">Your profile</legend>
<p>
Required fields are marked with an asterisk (<abbr
title="required"
class="usa-hint usa-hint--required"
>*</abbr
>).
</p>
{% for field in profile_form %}
<label class="usa-label" for="id_{{ field.name }}">{{ field.label }}</label>
{{ field }}
{% endfor %}
</fieldset>
<button type="submit" class="usa-button usa-button--big">Save Changes</button>
</form>
{% endblock content %}

View file

@ -1,5 +1,5 @@
from django.test import Client, TestCase
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
class TestViews(TestCase):
@ -16,23 +16,32 @@ class TestViews(TestCase):
self.assertContains(response, "registrar", status_code=200)
self.assertContains(response, "log in")
def test_whoami_page(self):
"""User information appears on the whoami page."""
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
user = User.objects.create(
username=username, first_name=first_name, last_name=last_name, email=email
)
self.client.force_login(user)
response = self.client.get("/whoami")
self.assertContains(response, first_name)
self.assertContains(response, last_name)
self.assertContains(response, email)
def test_whoami_page_no_user(self):
"""Whoami page not accessible without a logged-in user."""
response = self.client.get("/whoami")
self.assertEqual(response.status_code, 302)
self.assertIn("?next=/whoami", response.headers["Location"])
class LoggedInTests(TestCase):
def setUp(self):
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
self.user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email
)
self.client.force_login(user)
def test_whoami_page(self):
"""User information appears on the whoami page."""
response = self.client.get("/whoami")
self.assertContains(response, first_name)
self.assertContains(response, last_name)
self.assertContains(response, email)
def test_edit_profile(self):
response = self.client.get("/edit_profile/")
self.assertContains(response, "Display Name")

View file

@ -0,0 +1,22 @@
from django.shortcuts import redirect, render
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from ..forms import EditProfileForm
@login_required
def edit_profile(request):
"""View for a profile editing page."""
if request.method == "POST":
# post to this view when changes are made
profile_form = EditProfileForm(request.POST, instance=request.user.userprofile)
if profile_form.is_valid():
profile_form.save()
messages.success(request, "Your profile is updated successfully")
return redirect(to="edit-profile")
else:
profile_form = EditProfileForm(instance=request.user.userprofile)
return render(request, "profile.html", {"profile_form": profile_form})