mirror of
https://github.com/lemeow125/DRF_Template.git
synced 2025-09-18 05:29:37 +08:00
Implement accounts app
This commit is contained in:
parent
82c48cf5eb
commit
bae2cc653e
17 changed files with 519 additions and 11 deletions
0
src/accounts/__init__.py
Normal file
0
src/accounts/__init__.py
Normal file
20
src/accounts/admin.py
Normal file
20
src/accounts/admin.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
"""
|
||||||
|
Admin configuration for accounts app
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
from .models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
model = CustomUser
|
||||||
|
list_display = (
|
||||||
|
"id",
|
||||||
|
"is_active",
|
||||||
|
"is_new",
|
||||||
|
) + UserAdmin.list_display
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(CustomUser, CustomUserAdmin)
|
9
src/accounts/apps.py
Normal file
9
src/accounts/apps.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "accounts"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals # noqa: F401
|
131
src/accounts/migrations/0001_initial.py
Normal file
131
src/accounts/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
# Generated by Django 5.2.5 on 2025-09-02 17:51
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.utils.timezone
|
||||||
|
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="CustomUser",
|
||||||
|
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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
src/accounts/migrations/__init__.py
Normal file
0
src/accounts/migrations/__init__.py
Normal file
27
src/accounts/models.py
Normal file
27
src/accounts/models.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
"""
|
||||||
|
Common model schemas
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUser(AbstractUser):
|
||||||
|
# Most fields are inherited from AbstractUser
|
||||||
|
|
||||||
|
# Can be used to show tooltips for newer users
|
||||||
|
@property
|
||||||
|
def is_new(self):
|
||||||
|
current_date = timezone.now()
|
||||||
|
return self.date_joined + timedelta(days=1) < current_date
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def admin_url(self):
|
||||||
|
return reverse("admin:users_customuser_change", args=(self.pk,))
|
90
src/accounts/serializers.py
Normal file
90
src/accounts/serializers.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
from django.contrib.auth.password_validation import validate_password
|
||||||
|
from django.core import exceptions as django_exceptions
|
||||||
|
from django.core.cache import cache
|
||||||
|
from djoser.serializers import UserSerializer as BaseUserSerializer
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.serializers import ImageField, ModelSerializer
|
||||||
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
from accounts.models import CustomUser
|
||||||
|
|
||||||
|
# There can be multiple subject instances with the same name, only differing in course, year level, and semester. We filter them here
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleCustomUserSerializer(ModelSerializer):
|
||||||
|
class Meta(BaseUserSerializer.Meta):
|
||||||
|
model = CustomUser
|
||||||
|
fields = ("id", "username", "email", "full_name")
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserSerializer(BaseUserSerializer):
|
||||||
|
class Meta(BaseUserSerializer.Meta):
|
||||||
|
model = CustomUser
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
"first_name",
|
||||||
|
"is_new",
|
||||||
|
"last_name",
|
||||||
|
)
|
||||||
|
read_only_fields = (
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
cache.delete(f"users:{instance.id}")
|
||||||
|
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class UserRegistrationSerializer(serializers.ModelSerializer):
|
||||||
|
email = serializers.EmailField(required=True)
|
||||||
|
username = serializers.CharField(required=True)
|
||||||
|
password = serializers.CharField(
|
||||||
|
write_only=True, style={"input_type": "password", "placeholder": "Password"}
|
||||||
|
)
|
||||||
|
first_name = serializers.CharField(
|
||||||
|
required=True, allow_blank=False, allow_null=False
|
||||||
|
)
|
||||||
|
last_name = serializers.CharField(
|
||||||
|
required=True, allow_blank=False, allow_null=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CustomUser
|
||||||
|
fields = ["email", "username", "password", "first_name", "last_name"]
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
user_attrs = attrs.copy()
|
||||||
|
user = self.Meta.model(**user_attrs)
|
||||||
|
password = attrs.get("password")
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_password(password, user)
|
||||||
|
except django_exceptions.ValidationError as e:
|
||||||
|
serializer_error = serializers.as_serializer_error(e)
|
||||||
|
errors = serializer_error[api_settings.NON_FIELD_ERRORS_KEY]
|
||||||
|
if len(errors) > 1:
|
||||||
|
raise serializers.ValidationError({"password": errors[0]})
|
||||||
|
else:
|
||||||
|
raise serializers.ValidationError({"password": errors})
|
||||||
|
if self.Meta.model.objects.filter(username=attrs.get("username")).exists():
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"A user with that username already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().validate(attrs)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
user = self.Meta.model(**validated_data)
|
||||||
|
user.username = validated_data["username"]
|
||||||
|
user.is_active = False
|
||||||
|
user.set_password(validated_data["password"])
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
cache.delete("users")
|
||||||
|
|
||||||
|
return user
|
25
src/accounts/signals.py
Normal file
25
src/accounts/signals.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"""
|
||||||
|
Signal handlers for accounts app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db.models.signals import post_migrate
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from core.settings import config
|
||||||
|
from tests import users
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_migrate)
|
||||||
|
def generate_test_users(sender, **kwargs):
|
||||||
|
"""
|
||||||
|
Post-migrate signal to create test users in DEBUG mode.
|
||||||
|
"""
|
||||||
|
if sender.name == "accounts" and config.DEBUG:
|
||||||
|
try:
|
||||||
|
users.generate_test_users()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating test users post-migration: {e}")
|
17
src/accounts/urls.py
Normal file
17
src/accounts/urls.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
"""
|
||||||
|
URL configuration for accounts app
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.urls import include, path
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from accounts import views
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r"users", views.CustomUserViewSet, basename="users")
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", include(router.urls)),
|
||||||
|
path("", include("djoser.urls")),
|
||||||
|
path("", include("djoser.urls.jwt")),
|
||||||
|
]
|
76
src/accounts/views.py
Normal file
76
src/accounts/views.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"""
|
||||||
|
Viewset for accounts app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
|
from django.core.cache import cache
|
||||||
|
from djoser.conf import settings
|
||||||
|
from djoser.views import UserViewSet as DjoserUserViewSet
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
|
from accounts import serializers
|
||||||
|
from accounts.models import CustomUser
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserViewSet(DjoserUserViewSet):
|
||||||
|
queryset = CustomUser.objects.all()
|
||||||
|
serializer_class = serializers.CustomUserSerializer
|
||||||
|
permission_classes = settings.PERMISSIONS.activation
|
||||||
|
token_generator = default_token_generator
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
|
||||||
|
if user.is_superuser:
|
||||||
|
cache_key = "users:admin"
|
||||||
|
queryset = cache.get(cache_key)
|
||||||
|
if not queryset:
|
||||||
|
queryset = CustomUser.objects.all()
|
||||||
|
cache.set(cache_key, queryset, 60 * 60)
|
||||||
|
return queryset
|
||||||
|
else:
|
||||||
|
cache_key = f"users:{user.id}"
|
||||||
|
queryset = cache.get(cache_key)
|
||||||
|
if not queryset:
|
||||||
|
queryset = CustomUser.objects.filter(id=user.id)
|
||||||
|
cache.set(cache_key, queryset, 60 * 60)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def perform_update(self, serializer, *args, **kwargs):
|
||||||
|
user = self.request.user
|
||||||
|
|
||||||
|
super().perform_update(serializer, *args, **kwargs)
|
||||||
|
|
||||||
|
cache.delete("users")
|
||||||
|
cache.delete(f"users:{user.id}")
|
||||||
|
|
||||||
|
def perform_create(self, serializer, *args, **kwargs):
|
||||||
|
user = serializer.save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Try-catch block for email sending
|
||||||
|
try:
|
||||||
|
super().perform_create(serializer, *args, **kwargs)
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
cache.delete("users")
|
||||||
|
cache.delete(f"user:{user.id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Registration failure, unable to send activation email for {user.id}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@action(
|
||||||
|
methods=["post"], detail=False, url_path="activation", url_name="activation"
|
||||||
|
)
|
||||||
|
def activation(self, request, *args, **kwargs):
|
||||||
|
user = self.request.user
|
||||||
|
super().activation(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
cache.delete("users")
|
||||||
|
cache.delete(f"users:{user.id}")
|
|
@ -1,4 +1,3 @@
|
||||||
from core.settings import config
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from drf_spectacular.views import (
|
from drf_spectacular.views import (
|
||||||
|
@ -7,8 +6,10 @@ from drf_spectacular.views import (
|
||||||
SpectacularSwaggerView,
|
SpectacularSwaggerView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from core.settings import config
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path("accounts/", include("accounts.urls")),
|
path("accounts/", include("accounts.urls")),
|
||||||
# Admin Panel
|
# Admin Panel
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
# Swagger and Redoc API Doc URLs
|
# Swagger and Redoc API Doc URLs
|
||||||
|
|
|
@ -6,9 +6,11 @@ For use in the immediate parent app/directory.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic.fields import FieldInfo
|
|
||||||
from .models import Config as ConfigModel
|
|
||||||
from dotenv import find_dotenv, load_dotenv
|
from dotenv import find_dotenv, load_dotenv
|
||||||
|
from pydantic.fields import FieldInfo
|
||||||
|
|
||||||
|
from .models import Config as ConfigModel
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|
|
@ -3,13 +3,14 @@ Common model schemas
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Literal
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
BaseModel,
|
BaseModel,
|
||||||
StrictStr,
|
|
||||||
EmailStr,
|
EmailStr,
|
||||||
Field,
|
Field,
|
||||||
|
StrictStr,
|
||||||
field_validator,
|
field_validator,
|
||||||
model_validator,
|
model_validator,
|
||||||
)
|
)
|
||||||
|
@ -42,10 +43,8 @@ class Config(BaseModel):
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether to serve media files locally as oppossed to using a cloud storage solution.",
|
description="Whether to serve media files locally as oppossed to using a cloud storage solution.",
|
||||||
)
|
)
|
||||||
SMTP_HOST: StrictStr = Field(
|
SMTP_HOST: StrictStr = Field(required=True, description="SMTP server address")
|
||||||
required=True, description="SMTP server address")
|
SMTP_PORT: int = Field(default=587, description="SMTP server port (default: 587)")
|
||||||
SMTP_PORT: int = Field(
|
|
||||||
default=587, description="SMTP server port (default: 587)")
|
|
||||||
SMTP_USE_TLS: bool = Field(
|
SMTP_USE_TLS: bool = Field(
|
||||||
default=True, description="Whether to use TLS for SMTP connections"
|
default=True, description="Whether to use TLS for SMTP connections"
|
||||||
)
|
)
|
||||||
|
@ -64,6 +63,9 @@ class Config(BaseModel):
|
||||||
REFRESH_TOKEN_LIFETIME_DAYS: timedelta = Field(
|
REFRESH_TOKEN_LIFETIME_DAYS: timedelta = Field(
|
||||||
default=timedelta(days=3), description="Refresh token lifetime in days"
|
default=timedelta(days=3), description="Refresh token lifetime in days"
|
||||||
)
|
)
|
||||||
|
DEBUG_USER_PASSWORD: StrictStr = Field(
|
||||||
|
required=True, description="Password for test users created during development"
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator("CORS_ORIGINS", "ALLOWED_HOSTS", mode="before")
|
@field_validator("CORS_ORIGINS", "ALLOWED_HOSTS", mode="before")
|
||||||
def parse_list(cls, v):
|
def parse_list(cls, v):
|
||||||
|
|
21
src/core/management/commands/generate_test_users.py
Normal file
21
src/core/management/commands/generate_test_users.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
"""
|
||||||
|
Post-migrate signal handlers for creating initial data for accounts app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from tests.users import generate_test_users
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Generate debug admin and test user accounts."
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Command handling
|
||||||
|
"""
|
||||||
|
generate_test_users()
|
|
@ -12,13 +12,18 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from core.config import Config
|
from core.config import Config
|
||||||
|
|
||||||
# Config initialization
|
# Config initialization
|
||||||
config = Config().get_config()
|
config = Config().get_config()
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Directory where manage.py file is located
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
# Directory where docker-compose.yml file is located
|
||||||
|
ROOT_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
# Directory where test files are located
|
||||||
|
TESTS_DIR = os.path.join(BASE_DIR, "tests")
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = config.SECRET_KEY
|
SECRET_KEY = config.SECRET_KEY
|
||||||
|
@ -48,6 +53,8 @@ INSTALLED_APPS = [
|
||||||
"corsheaders",
|
"corsheaders",
|
||||||
"djoser",
|
"djoser",
|
||||||
"drf_spectacular",
|
"drf_spectacular",
|
||||||
|
"drf_spectacular_sidecar",
|
||||||
|
"accounts",
|
||||||
*(["silk"] if config.DEBUG else []),
|
*(["silk"] if config.DEBUG else []),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -227,6 +234,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = "accounts.CustomUser"
|
||||||
|
|
||||||
# Swagger / OpenAPI
|
# Swagger / OpenAPI
|
||||||
SPECTACULAR_SETTINGS = {
|
SPECTACULAR_SETTINGS = {
|
||||||
|
|
47
src/tests/users/__init__.py
Normal file
47
src/tests/users/__init__.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
"""
|
||||||
|
Post-migrate signal handlers for creating initial data for accounts app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from accounts.models import CustomUser
|
||||||
|
from core.settings import config, TESTS_DIR
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_test_users():
|
||||||
|
"""
|
||||||
|
Function to create test users in DEBUG mode.
|
||||||
|
"""
|
||||||
|
if config.DEBUG:
|
||||||
|
with open(os.path.join(TESTS_DIR, "users", "users.json"), "r") as f:
|
||||||
|
# Load JSON data
|
||||||
|
data = json.loads(f.read())
|
||||||
|
for user in data["users"]:
|
||||||
|
# Check if user already exists
|
||||||
|
USER = CustomUser.objects.filter(email=user["email"]).first()
|
||||||
|
if not USER:
|
||||||
|
# Create user
|
||||||
|
if user["is_superuser"]:
|
||||||
|
USER = CustomUser.objects.create_superuser(
|
||||||
|
username=user["username"],
|
||||||
|
email=user["email"],
|
||||||
|
password=config.DEBUG_USER_PASSWORD,
|
||||||
|
)
|
||||||
|
print("Created Superuser:", user["email"])
|
||||||
|
else:
|
||||||
|
USER = CustomUser.objects.create_user(
|
||||||
|
username=user["email"],
|
||||||
|
email=user["email"],
|
||||||
|
password=config.DEBUG_USER_PASSWORD,
|
||||||
|
)
|
||||||
|
print("Created User:", user["email"])
|
||||||
|
|
||||||
|
# Additional user fields not covered by create() methods
|
||||||
|
USER.first_name = user["first_name"]
|
||||||
|
USER.last_name = user["last_name"]
|
||||||
|
USER.is_active = True
|
||||||
|
USER.save()
|
32
src/tests/users/users.json
Normal file
32
src/tests/users/users.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"email": "admin@test.com",
|
||||||
|
"is_superuser": true,
|
||||||
|
"first_name": "Test",
|
||||||
|
"last_name": "Admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "testuser1",
|
||||||
|
"email": "user1@test.com",
|
||||||
|
"is_superuser": false,
|
||||||
|
"first_name": "Test",
|
||||||
|
"last_name": "User 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "testuser2",
|
||||||
|
"email": "user1@test.com",
|
||||||
|
"is_superuser": false,
|
||||||
|
"first_name": "Test",
|
||||||
|
"last_name": "User 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "testuser3",
|
||||||
|
"email": "user3@test.com",
|
||||||
|
"is_superuser": false,
|
||||||
|
"first_name": "Test",
|
||||||
|
"last_name": "User 3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue