diff --git a/src/.flake8 b/src/.flake8 index 42ef88a71..8c4d4851a 100644 --- a/src/.flake8 +++ b/src/.flake8 @@ -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/* diff --git a/src/registrar/admin.py b/src/registrar/admin.py new file mode 100644 index 000000000..dab331c22 --- /dev/null +++ b/src/registrar/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User, UserProfile + + +# edit a user's profile on the user page +class UserProfileInline(admin.StackedInline): + model = UserProfile + + +class MyUserAdmin(UserAdmin): + inlines = [UserProfileInline] + + +admin.site.register(User, MyUserAdmin) diff --git a/src/registrar/apps.py b/src/registrar/apps.py new file mode 100644 index 000000000..9f1b186ad --- /dev/null +++ b/src/registrar/apps.py @@ -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 diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 03dba9bc1..699bec794 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -54,6 +54,9 @@ BASE_DIR = path.resolve().parent.parent # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env_debug +# Use our user model instead of the default +AUTH_USER_MODEL = "registrar.User" + # Applications are modular pieces of code. # They are provided by Django, by third-parties, or by yourself. @@ -504,6 +507,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",) diff --git a/src/registrar/migrations/0001_initial.py b/src/registrar/migrations/0001_initial.py new file mode 100644 index 000000000..df709410c --- /dev/null +++ b/src/registrar/migrations/0001_initial.py @@ -0,0 +1,168 @@ +# Generated by Django 4.1.1 on 2022-09-22 16:05 + +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.CharField(max_length=2)), + ("street2", models.CharField(max_length=2)), + ("street3", models.CharField(max_length=2)), + ("city", models.CharField(max_length=2)), + ("sp", models.CharField(max_length=2)), + ("pc", models.CharField(max_length=2)), + ("cc", models.CharField(max_length=2)), + ("voice", models.CharField(max_length=2)), + ("fax", models.CharField(max_length=2)), + ("email", models.CharField(max_length=2)), + ("display_name", models.TextField()), + ( + "user", + models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={"abstract": False}, + ), + ] diff --git a/src/registrar/migrations/0002_alter_userprofile_cc_alter_userprofile_city_and_more.py b/src/registrar/migrations/0002_alter_userprofile_cc_alter_userprofile_city_and_more.py new file mode 100644 index 000000000..e42b14ae1 --- /dev/null +++ b/src/registrar/migrations/0002_alter_userprofile_cc_alter_userprofile_city_and_more.py @@ -0,0 +1,63 @@ +# Generated by Django 4.1.1 on 2022-09-22 16:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="userprofile", + name="cc", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="userprofile", + name="city", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="userprofile", + name="email", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="userprofile", + name="fax", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="userprofile", + name="pc", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="userprofile", + name="sp", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="userprofile", + name="street1", + field=models.TextField(), + ), + migrations.AlterField( + model_name="userprofile", + name="street2", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="userprofile", + name="street3", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="userprofile", + name="voice", + field=models.TextField(blank=True), + ), + ] diff --git a/src/registrar/migrations/0003_alter_userprofile_street1.py b/src/registrar/migrations/0003_alter_userprofile_street1.py new file mode 100644 index 000000000..84e7f485a --- /dev/null +++ b/src/registrar/migrations/0003_alter_userprofile_street1.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.1 on 2022-09-22 16:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0002_alter_userprofile_cc_alter_userprofile_city_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="userprofile", + name="street1", + field=models.TextField(blank=True), + ), + ] diff --git a/src/registrar/migrations/__init__.py b/src/registrar/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py new file mode 100644 index 000000000..852dbad16 --- /dev/null +++ b/src/registrar/models/__init__.py @@ -0,0 +1,3 @@ +from .models import User, UserProfile + +__all__ = [User, UserProfile] diff --git a/src/registrar/models/models.py b/src/registrar/models/models.py new file mode 100644 index 000000000..5f96fe0ff --- /dev/null +++ b/src/registrar/models/models.py @@ -0,0 +1,81 @@ +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): + if self.userprofile.display_name: + return self.userprofile.display_name + else: + 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: + return self.user.username diff --git a/src/registrar/signals.py b/src/registrar/signals.py new file mode 100644 index 000000000..913e07004 --- /dev/null +++ b/src/registrar/signals.py @@ -0,0 +1,17 @@ +from django.db.models.signals import post_save +from django.contrib.auth.models import User +from django.dispatch import receiver + +from .models import UserProfile + + +@receiver(post_save, sender=User) +def create_profile(sender, instance, created, **kwargs): + if created: + UserProfile.objects.create(user=instance) + + +@receiver(post_save, sender=User) +def save_profile(sender, instance, **kwargs): + # instance is a User, it has a profile from the one-to-one relation + instance.userprofile.save()