mirror of
https://github.com/lemeow125/DRF_Template.git
synced 2025-09-18 05:29:37 +08:00
Move to uv/pyproject and overhaul project structure
This commit is contained in:
commit
fbb76f8196
26 changed files with 1981 additions and 0 deletions
0
src/api/__init__.py
Normal file
0
src/api/__init__.py
Normal file
22
src/api/urls.py
Normal file
22
src/api/urls.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from core.settings import config
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularRedocView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# path("accounts/", include("accounts.urls")),
|
||||
# Admin Panel
|
||||
path("admin/", admin.site.urls),
|
||||
# Swagger and Redoc API Doc URLs
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path(
|
||||
"swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"
|
||||
),
|
||||
path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
||||
# Silk Enabled on DEBUG
|
||||
*([path("silk/", include("silk.urls", namespace="silk"))] if config.DEBUG else []),
|
||||
]
|
0
src/core/__init__.py
Normal file
0
src/core/__init__.py
Normal file
16
src/core/asgi.py
Normal file
16
src/core/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for the project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
|
||||
|
||||
application = get_asgi_application()
|
64
src/core/config/__init__.py
Normal file
64
src/core/config/__init__.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
"""
|
||||
Common service functions and classes imported elsewhere.
|
||||
|
||||
For use in the immediate parent app/directory.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
from pydantic.fields import FieldInfo
|
||||
from .models import Config as ConfigModel
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
Core application config.
|
||||
"""
|
||||
|
||||
def __init__(self, prefix: Optional[str] = "backend") -> None:
|
||||
"""
|
||||
Initialize the Config class.
|
||||
|
||||
Args:
|
||||
prefix (str, optional): Prefix for environment variables. Defaults to "backend".
|
||||
|
||||
"""
|
||||
load_dotenv(find_dotenv())
|
||||
|
||||
self.prefix = prefix.lower()
|
||||
|
||||
for field in ConfigModel.model_fields.items():
|
||||
setattr(self, field[0], self.set_env_var(field))
|
||||
|
||||
def set_env_var(self, field: tuple):
|
||||
"""
|
||||
Retrieve and sets an environment variable.
|
||||
|
||||
Args:
|
||||
field (tuple): A tuple containing the field name and its FieldInfo.
|
||||
|
||||
Returns:
|
||||
str: The value of the environment variable.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If required is True and the variable is not set.
|
||||
"""
|
||||
|
||||
# Unpack field info
|
||||
field_key = f"{self.prefix}_{field[0]}".upper()
|
||||
field_info: FieldInfo = field[1]
|
||||
|
||||
# Fetch value, return field default value if not found
|
||||
field_value = os.getenv(field_key, field_info.default)
|
||||
|
||||
return field_value
|
||||
|
||||
def get_config(self) -> ConfigModel:
|
||||
"""
|
||||
Get the config model.
|
||||
|
||||
Returns:
|
||||
ConfigModel: The config model instance.
|
||||
"""
|
||||
return ConfigModel(**self.__dict__)
|
132
src/core/config/models.py
Normal file
132
src/core/config/models.py
Normal file
|
@ -0,0 +1,132 @@
|
|||
"""
|
||||
Common model schemas
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Literal
|
||||
from datetime import timedelta
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
StrictStr,
|
||||
EmailStr,
|
||||
Field,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic_extra_types.timezone_name import TimeZoneName
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
"""
|
||||
Pydantic Configuration model for Django
|
||||
"""
|
||||
|
||||
SECRET_KEY: StrictStr = Field(
|
||||
min_length=32, description="Secret key for the API", required=True
|
||||
)
|
||||
DEBUG: bool = Field(default=False, description="API debug mode")
|
||||
TIMEZONE: TimeZoneName = "UTC"
|
||||
CORS_ORIGINS: list[StrictStr] = Field(
|
||||
description="Allowed CORS origins for API.", default_factory=list
|
||||
)
|
||||
ALLOWED_HOSTS: list[StrictStr] = Field(
|
||||
description="Allowed hosts by the API.", default_factory=list
|
||||
)
|
||||
USE_TZ: bool = Field(
|
||||
required=True,
|
||||
default=True,
|
||||
description="Whether the backend API defaults to using timezone-aware datetimes.",
|
||||
)
|
||||
DJANGO_LOG_LEVEL: Literal["INFO", "DEBUG"] = "INFO"
|
||||
SERVE_MEDIA_LOCALLY: bool = Field(
|
||||
default=False,
|
||||
description="Whether to serve media files locally as oppossed to using a cloud storage solution.",
|
||||
)
|
||||
SMTP_HOST: StrictStr = Field(
|
||||
required=True, description="SMTP server address")
|
||||
SMTP_PORT: int = Field(
|
||||
default=587, description="SMTP server port (default: 587)")
|
||||
SMTP_USE_TLS: bool = Field(
|
||||
default=True, description="Whether to use TLS for SMTP connections"
|
||||
)
|
||||
SMTP_AUTH_USERNAME: StrictStr = Field(
|
||||
required=True, description="SMTP authentication username"
|
||||
)
|
||||
SMTP_AUTH_PASSWORD: StrictStr = Field(
|
||||
required=True, description="SMTP authentication password"
|
||||
)
|
||||
SMTP_FROM_ADDRESS: EmailStr = Field(
|
||||
required=True, description="SMTP from email address"
|
||||
)
|
||||
ACCESS_TOKEN_LIFETIME_MINUTES: timedelta = Field(
|
||||
default=timedelta(minutes=240), description="Access token lifetime in minutes"
|
||||
)
|
||||
REFRESH_TOKEN_LIFETIME_DAYS: timedelta = Field(
|
||||
default=timedelta(days=3), description="Refresh token lifetime in days"
|
||||
)
|
||||
|
||||
@field_validator("CORS_ORIGINS", "ALLOWED_HOSTS", mode="before")
|
||||
def parse_list(cls, v):
|
||||
"""
|
||||
Splits a comma-separated string into a list.
|
||||
"""
|
||||
if isinstance(v, str):
|
||||
return v.split(",")
|
||||
return v
|
||||
|
||||
@field_validator("ACCESS_TOKEN_LIFETIME_MINUTES", mode="before")
|
||||
def parse_timedelta_minutes(cls, v):
|
||||
"""
|
||||
Parse integer values into timedelta objects.
|
||||
"""
|
||||
if isinstance(v, str):
|
||||
return timedelta(minutes=int(v))
|
||||
return v
|
||||
|
||||
@field_validator("REFRESH_TOKEN_LIFETIME_DAYS", mode="before")
|
||||
def parse_timedelta_days(cls, v):
|
||||
"""
|
||||
Parse integer values into timedelta objects.
|
||||
"""
|
||||
if isinstance(v, str):
|
||||
return timedelta(days=int(v))
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def derive_token_lifetimes(cls, v):
|
||||
"""
|
||||
Sets the appropriate log level based on the DEBUG setting.
|
||||
"""
|
||||
if v.DEBUG:
|
||||
v.DJANGO_LOG_LEVEL = "DEBUG"
|
||||
else:
|
||||
v.DJANGO_LOG_LEVEL = "INFO"
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def derive_allowed_hosts(cls, v):
|
||||
"""
|
||||
Extracts additional hostnames from CORS_ORIGINS to append to ALLOWED_HOSTS.
|
||||
"""
|
||||
|
||||
cors_origins = v.CORS_ORIGINS
|
||||
allowed_hosts = set(v.ALLOWED_HOSTS or [])
|
||||
|
||||
for origin in cors_origins:
|
||||
match = re.match(r"https?://([^/]+)", origin)
|
||||
if match and match.group(1): # Ensure match.group(1) is not empty
|
||||
allowed_hosts.add(match.group(1))
|
||||
|
||||
v.ALLOWED_HOSTS = list(allowed_hosts)
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def derive_log_level(cls, v):
|
||||
"""
|
||||
Sets the appropriate log level based on the DEBUG setting.
|
||||
"""
|
||||
if v.DEBUG:
|
||||
v.DJANGO_LOG_LEVEL = "DEBUG"
|
||||
else:
|
||||
v.DJANGO_LOG_LEVEL = "INFO"
|
||||
return v
|
241
src/core/settings.py
Normal file
241
src/core/settings.py
Normal file
|
@ -0,0 +1,241 @@
|
|||
"""
|
||||
Django settings for the project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from core.config import Config
|
||||
|
||||
# Config initialization
|
||||
config = Config().get_config()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = config.SECRET_KEY
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = config.DEBUG
|
||||
|
||||
CORS_ALLOWED_ORIGINS = config.CORS_ORIGINS
|
||||
ALLOWED_HOSTS = config.ALLOWED_HOSTS
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"core",
|
||||
"unfold",
|
||||
"unfold.contrib.filters",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django_extensions",
|
||||
"rest_framework",
|
||||
"rest_framework_simplejwt",
|
||||
"corsheaders",
|
||||
"djoser",
|
||||
"drf_spectacular",
|
||||
*(["silk"] if config.DEBUG else []),
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
# Silk enabled on DEBUG
|
||||
*(["silk.middleware.SilkyMiddleware"] if config.DEBUG else []),
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
DJANGO_LOG_LEVEL = config.DJANGO_LOG_LEVEL
|
||||
DEBUG_PROPAGATE_EXCEPTIONS = [True if config.DEBUG else False]
|
||||
|
||||
ROOT_URLCONF = "core.urls"
|
||||
|
||||
# Email Templates
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [
|
||||
BASE_DIR / "core/templates/",
|
||||
],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "core.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
# Storage
|
||||
|
||||
STORAGES = {
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = config.TIMEZONE
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = config.USE_TZ
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# REST Framework
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
),
|
||||
"DEFAULT_THROTTLE_CLASSES": [
|
||||
"rest_framework.throttling.AnonRateThrottle",
|
||||
"rest_framework.throttling.UserRateThrottle",
|
||||
],
|
||||
"DEFAULT_THROTTLE_RATES": {"anon": "60/min", "user": "240/min"},
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
|
||||
# Authentication
|
||||
DJOSER = {
|
||||
"SEND_ACTIVATION_EMAIL": True,
|
||||
"SEND_CONFIRMATION_EMAIL": True,
|
||||
"PASSWORD_RESET_CONFIRM_URL": "reset_password/confirm/{uid}/{token}",
|
||||
"ACTIVATION_URL": "activation/{uid}/{token}",
|
||||
"USER_AUTHENTICATION_RULES": ["djoser.authentication.TokenAuthenticationRule"],
|
||||
"SERIALIZERS": {
|
||||
"user": "accounts.serializers.CustomUserSerializer",
|
||||
"current_user": "accounts.serializers.CustomUserSerializer",
|
||||
"user_create": "accounts.serializers.CustomUserRegistrationSerializer",
|
||||
},
|
||||
"PERMISSIONS": {
|
||||
# Unused endpoints set to admin only
|
||||
"username_reset": ["rest_framework.permissions.IsAdminUser"],
|
||||
"username_reset_confirm": ["rest_framework.permissions.IsAdminUser"],
|
||||
"set_username": ["rest_framework.permissions.IsAdminUser"],
|
||||
"set_password": ["rest_framework.permissions.IsAdminUser"],
|
||||
},
|
||||
}
|
||||
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": config.ACCESS_TOKEN_LIFETIME_MINUTES,
|
||||
"REFRESH_TOKEN_LIFETIME": config.REFRESH_TOKEN_LIFETIME_DAYS,
|
||||
"ROTATE_REFRESH_TOKENS": True,
|
||||
"BLACKLIST_AFTER_ROTATION": True,
|
||||
}
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
"OPTIONS": {
|
||||
"min_length": 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
# core/validators.py
|
||||
{
|
||||
"NAME": "core.validators.SpecialCharacterValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "core.validators.LowercaseValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "core.validators.UppercaseValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "core.validators.NumberValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Swagger / OpenAPI
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
}
|
||||
|
||||
# Pygraphviz
|
||||
GRAPH_MODELS = {
|
||||
"all_applications": True,
|
||||
"group_models": True,
|
||||
}
|
10
src/core/templates/__init__.py
Normal file
10
src/core/templates/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.utils import timezone
|
||||
from djoser import email
|
||||
|
||||
|
||||
class ActivationEmail(email.ActivationEmail):
|
||||
template_name = "email_activation.html"
|
||||
|
||||
|
||||
class PasswordResetEmail(email.PasswordResetEmail):
|
||||
template_name = "password_change.html"
|
28
src/core/templates/activation.html
Normal file
28
src/core/templates/activation.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block subject %}
|
||||
{% blocktrans %}Account activation on {{ site_name }}{% endblocktrans %}
|
||||
{% endblock subject %}
|
||||
|
||||
{% block text_body %}
|
||||
{% blocktrans %}You're receiving this email because you need to finish activation process on {{ site_name }}.{% endblocktrans %}
|
||||
|
||||
{% trans "Please go to the following page to activate account:" %}
|
||||
{{ protocol }}://{{ domain }}/{{ url|safe }}
|
||||
|
||||
{% trans "Thanks for using our site!" %}
|
||||
|
||||
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||
{% endblock text_body %}
|
||||
|
||||
{% block html_body %}
|
||||
<p>{% blocktrans %}You're receiving this email because you need to finish activation process on {{ site_name }}.{% endblocktrans %}</p>
|
||||
|
||||
<p>{% trans "Please go to the following page to activate account:" %}</p>
|
||||
<p><a href="{{ protocol }}://{{ domain }}/{{ url|safe }}">{{ protocol }}://{{ domain }}/{{ url|safe }}</a></p>
|
||||
|
||||
<p>{% trans "Thanks for using our site!" %}</p>
|
||||
|
||||
<p>{% blocktrans %}The {{ site_name }} team{% endblocktrans %}</p>
|
||||
|
||||
{% endblock html_body %}
|
29
src/core/templates/password_reset.html
Normal file
29
src/core/templates/password_reset.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block subject %}
|
||||
{% blocktrans %}Password reset on {{ site_name }}{% endblocktrans %}
|
||||
{% endblock subject %}
|
||||
|
||||
{% block text_body %}
|
||||
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
|
||||
|
||||
{% trans "Please go to the following page and choose a new password:" %}
|
||||
{{ protocol }}://{{ domain }}/{{ url|safe }}
|
||||
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
|
||||
|
||||
{% trans "Thanks for using our site!" %}
|
||||
|
||||
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||
{% endblock text_body %}
|
||||
|
||||
{% block html_body %}
|
||||
<p>{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}</p>
|
||||
|
||||
<p>{% trans "Please go to the following page and choose a new password:" %}</p>
|
||||
<a href="{{ protocol }}://{{ domain }}/{{ url|safe }}">{{ protocol }}://{{ domain }}/{{ url|safe }}</a>
|
||||
<p>{% trans "Your username, in case you've forgotten:" %} <b>{{ user.get_username }}</b></p>
|
||||
|
||||
<p>{% trans "Thanks for using our site!" %}</p>
|
||||
|
||||
<p>{% blocktrans %}The {{ site_name }} team{% endblocktrans %}</p>
|
||||
{% endblock html_body %}
|
11
src/core/urls.py
Normal file
11
src/core/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
Base URL configuration for the project.
|
||||
|
||||
Refer to api/urls.py for actual endpoints.
|
||||
"""
|
||||
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("api/v1/", include("api.urls")),
|
||||
]
|
56
src/core/validators.py
Normal file
56
src/core/validators.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
"""
|
||||
Custom password validators for enforcing password complexity.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class UppercaseValidator(object):
|
||||
def validate(self, password, user=None):
|
||||
if not re.findall("[A-Z]", password):
|
||||
raise ValidationError(
|
||||
_("The password must contain at least 1 uppercase letter (A-Z).")
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return _("Your password must contain at least 1 uppercase letter (A-Z).")
|
||||
|
||||
|
||||
class LowercaseValidator(object):
|
||||
def validate(self, password, user=None):
|
||||
if not re.findall("[a-z]", password):
|
||||
raise ValidationError(
|
||||
_("The password must contain at least 1 lowercase letter (a-z).")
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return _("Your password must contain at least 1 lowercase letter (a-z).")
|
||||
|
||||
|
||||
class SpecialCharacterValidator(object):
|
||||
def validate(self, password, user=None):
|
||||
if not re.findall("[@#$%^&*()_+/\<>;:!?]", password):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The password must contain at least 1 special character (@, #, $, etc.)."
|
||||
)
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return _(
|
||||
"Your password must contain at least 1 special character (@, #, $, etc.)."
|
||||
)
|
||||
|
||||
|
||||
class NumberValidator(object):
|
||||
def validate(self, password, user=None):
|
||||
if not any(char.isdigit() for char in password):
|
||||
raise ValidationError(
|
||||
_("The password must contain at least one numerical digit (0-9).")
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return _("Your password must contain at least numerical digit (0-9).")
|
16
src/core/wsgi.py
Normal file
16
src/core/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for the project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
|
||||
|
||||
application = get_wsgi_application()
|
23
src/manage.py
Normal file
23
src/manage.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue