Skip to content

Commit

Permalink
Added custom user model & covered app with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
n8creator committed Dec 12, 2023
1 parent a5b031e commit 2acdd42
Show file tree
Hide file tree
Showing 13 changed files with 385 additions and 9 deletions.
16 changes: 8 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@ install:

# Lint project
isort:
@poetry run isort task_manager
poetry run isort task_manager/

black:
@poetry run black task_manager
black: isort
poetry run black task_manager/

lint: black
@poetry run flake8 task_manager
lint: isort black
poetry run flake8 --config ./.flake8 task_manager/

# Run tests
test:
@poetry run ./manage.py test
poetry run pytest -vv --cov ./task_manager/ --cov-report term-missing:skip-covered

# Start & deploy project
start:
@poetry run ./manage.py runserver 0.0.0.0:8000
poetry run ./manage.py runserver 0.0.0.0:8000

migrate:
poetry run ./manage.py migrate
Expand All @@ -36,4 +36,4 @@ deploy:
# System commands for Makefile
MAKEFLAGS += --no-print-directory

.PHONY: requirements install isort black lint test start migrate deploy
.PHONY: requirements install isort black lint test start migrate deploy
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ pytest-django = "^4.7.0"


[tool.poetry.group.code-quality.dependencies]
flake8 = "^6.1.0"
isort = "^5.13.1"
black = "^23.12.0"
djlint = "^1.34.0"
flake8 = "^6.1.0"


[tool.poetry.group.utils.dependencies]
Expand Down
12 changes: 12 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[pytest]
minversion = 7.0
addopts = -n 8
python_files =
test_*.py
*_test.py
*.py

filterwarnings =
ignore::DeprecationWarning

DJANGO_SETTINGS_MODULE = task_manager.settings
5 changes: 5 additions & 0 deletions task_manager/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Custom apps
"users",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -121,3 +123,6 @@
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# Set Custom User Model
AUTH_USER_MODEL = "users.User"
Empty file added users/__init__.py
Empty file.
102 changes: 102 additions & 0 deletions users/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""
Django Admin Customization For Users.
"""

from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError

from users.models import User


class UserCreationForm(forms.ModelForm):
"""
A form for creating a new users. Includes all the required fields, plus a repeated password.
"""

password1 = forms.CharField(label="Password", widget=forms.PasswordInput)
password2 = forms.CharField(
label="Password confirmation", widget=forms.PasswordInput
)

class Meta:
model = User
fields = ["email"]

def clean_password2(self):
# Check that the two passwords entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise ValidationError("Passwords don't match!")
return password2

def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user


class UserChangeForm(forms.ModelForm):
"""
A form for updating Users.
"""

# Replace password field with admin's disabled password hash display field
password = ReadOnlyPasswordHashField() # type: ignore

class Meta:
model = User
fields = ["email", "is_active", "is_admin"]


class UserAdmin(BaseUserAdmin):
"""
Define Admin Page for Users.
"""

# The forms to add and change user instances.
form = UserChangeForm
add_form = UserCreationForm

# The fields to be used in displaying the User model.
ordering = ["id"]
list_display = ["email", "name", "last_login", "is_admin"]
list_filter = ["is_admin"]
search_fields = ["email"]
fieldsets = [
(None, {"fields": ["name", "email", "password"]}),
("Permissions", {"fields": ["is_active", "is_staff", "is_superuser"]}),
("System info", {"fields": ["last_login"]}),
]

filter_horizontal = []
readonly_fields = ["last_login", "password"]

# Fieldsets used when creating a user.
add_fieldsets = [
(
None,
{
"classes": ["wide"],
"fields": [
"email",
"password1",
"password2",
"name",
"is_active",
"is_staff",
"is_superuser",
],
},
),
]


# Register User models in admin
admin.site.register(User, UserAdmin)
6 changes: 6 additions & 0 deletions users/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"
71 changes: 71 additions & 0 deletions users/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Generated by Django 5.0 on 2023-12-12 14:29

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"
),
),
(
"email",
models.EmailField(
max_length=255, unique=True, verbose_name="email address"
),
),
("name", models.CharField(max_length=255)),
("is_active", models.BooleanField(default=True)),
("is_admin", models.BooleanField(default=False)),
("is_staff", models.BooleanField(default=False)),
("is_superuser", 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={
"abstract": False,
},
),
]
Empty file added users/migrations/__init__.py
Empty file.
72 changes: 72 additions & 0 deletions users/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
Custom User and Manager models.
"""

from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
from django.db import models


class UserManager(BaseUserManager):
"""
Manager for Users in the system.
"""

def create_user(self, email, password=None, **extra_fields):
"""
Creates and saves a User with the given email, password and extra agruments (if any).
"""
if not email:
raise ValueError("Users must have an email address!")

user = self.model(
email=self.normalize_email(email),
**extra_fields,
)
user.set_password(password)
user.save(using=self._db)

return user

def create_superuser(self, email, password=None, **extra_fields):
"""
Creates and saves a superuser with the given email, password and extra arguments (if any).
"""

user = self.create_user(
email=email,
password=password,
)
user.is_staff = True
user.is_admin = True
user.is_superuser = True
user.save(using=self._db)

return user


class User(AbstractBaseUser, PermissionsMixin):
"""
Base User in the system.
"""

email = models.EmailField(
verbose_name="email address",
max_length=255,
unique=True,
)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)

objects = UserManager()

USERNAME_FIELD = "email"

def __str__(self):
return self.email
Loading

0 comments on commit 2acdd42

Please sign in to comment.