diff --git a/apps/users/__init__.py b/apps/users/__init__.py new file mode 100644 index 0000000..c38d98c --- /dev/null +++ b/apps/users/__init__.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + +default_app_config = "apps.users.UsersConfig" + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.users" diff --git a/apps/users/admin.py b/apps/users/admin.py new file mode 100644 index 0000000..054f3f5 --- /dev/null +++ b/apps/users/admin.py @@ -0,0 +1,34 @@ +from django.contrib.admin import register +from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin +from django.utils.translation import gettext_lazy as _ + +from apps.users.models import User + + +@register(User) +class UserAdmin(DefaultUserAdmin): + list_display = ("username", "email", "is_staff", "is_active") + fieldsets = [ + (_("Personal info"), {"fields": ("username", "email", "password")}), + ( + _("Permissions"), + { + "fields": ( + "user_permissions", + "groups", + "is_active", + "is_staff", + "is_superuser", + ) + } + ), + ] + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("username", "email", "password1", "password2"), + }, + ), + ) diff --git a/apps/users/managers.py b/apps/users/managers.py new file mode 100644 index 0000000..c79e1a3 --- /dev/null +++ b/apps/users/managers.py @@ -0,0 +1,37 @@ +from typing import TYPE_CHECKING, Any + +from django.contrib.auth.models import BaseUserManager + +if TYPE_CHECKING: + from apps.users.models import User +else: + User = Any + + +class UserManager(BaseUserManager[User]): + def _create_user( + self, + username: str, + email: str, + password: str, + **fields: bool, + ) -> User: + email = self.normalize_email(email) + user = self.model(username=username, email=email, **fields) + + user.set_password(password) + user.save() + + return user + + def create_user(self, username: str, email: str, password: str, **fields: bool) -> User: + fields.setdefault("is_staff", False) + fields.setdefault("is_superuser", False) + + return self._create_user(username, email, password, **fields) + + def create_superuser(self, username: str, email: str, password: str, **fields: bool) -> User: + fields["is_staff"] = True + fields["is_superuser"] = True + + return self._create_user(username, email, password, **fields) diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py new file mode 100644 index 0000000..4e757db --- /dev/null +++ b/apps/users/migrations/0001_initial.py @@ -0,0 +1,87 @@ +# Generated by Django 4.2.5 on 2023-09-30 02:51 + +from django.db import migrations, models + + +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", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "email", + models.EmailField( + db_index=True, max_length=256, unique=True + ), + ), + ( + "username", + models.CharField( + db_index=True, max_length=128, unique=True + ), + ), + ("is_active", models.BooleanField(default=True)), + ("is_staff", models.BooleanField(default=False)), + ( + "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={ + "db_table": "users", + }, + ), + ] diff --git a/apps/users/migrations/__init__.py b/apps/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/users/models.py b/apps/users/models.py new file mode 100644 index 0000000..ce74767 --- /dev/null +++ b/apps/users/models.py @@ -0,0 +1,33 @@ +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.db.models import EmailField, CharField, BooleanField + +from core.models import TimestampedModel +from apps.users.managers import UserManager + + +class User(AbstractBaseUser, PermissionsMixin, TimestampedModel): + """Represents an user.""" + + email = EmailField(db_index=True, max_length=256, unique=True) + username = CharField(db_index=True, max_length=128, unique=True) + + # When a user no longer wishes to use our platform, they may try to + # delete there account. That's a problem for us because the data we + # collect is valuable to us and we don't want to delete it. To solve + # this problem, we will simply offer users a way to deactivate their + # account instead of letting them delete it. That way they won't + # show up on the site anymore, but we can still analyze the data. + is_active = BooleanField(default=True) + + # Designates whether the user can log into the admin site. + is_staff = BooleanField(default=False) + + # Telling Django that the email field should be used for + # authentication instead of the username field. + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["username"] + + objects = UserManager() + + class Meta: + db_table = "users" diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..28e802b --- /dev/null +++ b/core/models.py @@ -0,0 +1,15 @@ +from django.db.models import Model, DateTimeField + + +class TimestampedModel(Model): + created_at = DateTimeField(auto_now_add=True) + updated_at = DateTimeField(auto_now=True) + + class Meta: + abstract = True + + # By default, any model that inherits from this class should be + # ordered in reverse-chronological order. We can override this + # on a per-model basis as needed, but reverse-chronological is a + # good default ordering for most models. + ordering = ["-created_at", "-updated_at"] diff --git a/server/settings/base.py b/server/settings/base.py index 4abcfea..a9d70f5 100644 --- a/server/settings/base.py +++ b/server/settings/base.py @@ -75,7 +75,9 @@ "bootstrap5", ] -LOCAL_APPS: List[str] = [] +LOCAL_APPS = [ + "apps.users" +] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS @@ -137,6 +139,11 @@ LOGIN_URL = "login" +# The model to use to represent an user. +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-user-model + +AUTH_USER_MODEL = "users.User" + ##################### # Django Guardian # ##################### diff --git a/templates/base.html b/templates/base.html index 0368e99..4f60038 100644 --- a/templates/base.html +++ b/templates/base.html @@ -69,6 +69,9 @@ {{ user.username }}