Add environment configuration and email settings
- Created a new .env.example file for environment variable configuration including Django settings, email configurations, and common SMTP provider examples. - Updated .vscode/settings.json to include additional words for spell checking. - Enhanced config/settings.py to load environment variables using python-dotenv, added data directory management, and configured email settings. - Updated config/urls.py to include debug toolbar URLs conditionally based on testing. - Added pytest configuration in conftest.py for Django testing. - Created core application with custom User model, admin registration, and migrations. - Implemented tests for User model and admin functionalities. - Updated pyproject.toml to include new dependencies for debugging and environment management. - Updated uv.lock to reflect new package versions and dependencies.
This commit is contained in:
parent
11c3db9817
commit
96a159f691
16 changed files with 697 additions and 57 deletions
68
.env.example
Normal file
68
.env.example
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Django Configuration
|
||||||
|
# Set to False in production
|
||||||
|
DEBUG=True
|
||||||
|
|
||||||
|
# Django Secret Key
|
||||||
|
# Generate a new secret key for production: python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
|
||||||
|
DJANGO_SECRET_KEY=your-secret-key-here
|
||||||
|
|
||||||
|
# Email Configuration
|
||||||
|
# SMTP Host (examples below)
|
||||||
|
EMAIL_HOST=smtp.gmail.com
|
||||||
|
|
||||||
|
# SMTP Port (common ports: 587 for TLS, 465 for SSL, 25 for unencrypted)
|
||||||
|
EMAIL_PORT=587
|
||||||
|
|
||||||
|
# Email credentials
|
||||||
|
EMAIL_HOST_USER=your-email@gmail.com
|
||||||
|
EMAIL_HOST_PASSWORD=your-app-password-here
|
||||||
|
|
||||||
|
# Connection security
|
||||||
|
# Use TLS (True for most providers like Gmail, Outlook)
|
||||||
|
EMAIL_USE_TLS=True
|
||||||
|
# Use SSL (False for most providers, True for some like older configurations)
|
||||||
|
EMAIL_USE_SSL=False
|
||||||
|
|
||||||
|
# Connection timeout in seconds
|
||||||
|
EMAIL_TIMEOUT=10
|
||||||
|
|
||||||
|
# Common SMTP Provider Examples:
|
||||||
|
#
|
||||||
|
# Gmail:
|
||||||
|
# EMAIL_HOST=smtp.gmail.com
|
||||||
|
# EMAIL_PORT=587
|
||||||
|
# EMAIL_USE_TLS=True
|
||||||
|
# EMAIL_USE_SSL=False
|
||||||
|
# Note: Requires App Password with 2FA enabled
|
||||||
|
#
|
||||||
|
# Outlook/Hotmail:
|
||||||
|
# EMAIL_HOST=smtp-mail.outlook.com
|
||||||
|
# EMAIL_PORT=587
|
||||||
|
# EMAIL_USE_TLS=True
|
||||||
|
# EMAIL_USE_SSL=False
|
||||||
|
#
|
||||||
|
# Yahoo:
|
||||||
|
# EMAIL_HOST=smtp.mail.yahoo.com
|
||||||
|
# EMAIL_PORT=587
|
||||||
|
# EMAIL_USE_TLS=True
|
||||||
|
# EMAIL_USE_SSL=False
|
||||||
|
#
|
||||||
|
# SendGrid:
|
||||||
|
# EMAIL_HOST=smtp.sendgrid.net
|
||||||
|
# EMAIL_PORT=587
|
||||||
|
# EMAIL_USE_TLS=True
|
||||||
|
# EMAIL_USE_SSL=False
|
||||||
|
# EMAIL_HOST_USER=apikey
|
||||||
|
# EMAIL_HOST_PASSWORD=your-sendgrid-api-key
|
||||||
|
#
|
||||||
|
# Mailgun:
|
||||||
|
# EMAIL_HOST=smtp.mailgun.org
|
||||||
|
# EMAIL_PORT=587
|
||||||
|
# EMAIL_USE_TLS=True
|
||||||
|
# EMAIL_USE_SSL=False
|
||||||
|
#
|
||||||
|
# Amazon SES:
|
||||||
|
# EMAIL_HOST=email-smtp.us-east-1.amazonaws.com
|
||||||
|
# EMAIL_PORT=587
|
||||||
|
# EMAIL_USE_TLS=True
|
||||||
|
# EMAIL_USE_SSL=False
|
||||||
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
|
|
@ -1,8 +1,22 @@
|
||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"adminpass",
|
||||||
|
"apikey",
|
||||||
|
"appauthor",
|
||||||
|
"appname",
|
||||||
"ASGI",
|
"ASGI",
|
||||||
|
"docstrings",
|
||||||
|
"dotenv",
|
||||||
|
"Hellsén",
|
||||||
"isort",
|
"isort",
|
||||||
|
"Joakim",
|
||||||
|
"lovinator",
|
||||||
|
"Mailgun",
|
||||||
"pydocstyle",
|
"pydocstyle",
|
||||||
"ttvdrops"
|
"regularuser",
|
||||||
|
"sendgrid",
|
||||||
|
"testpass",
|
||||||
|
"ttvdrops",
|
||||||
|
"wrongpassword"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,24 +1,105 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
from dotenv import load_dotenv
|
||||||
|
from platformdirs import user_data_dir
|
||||||
|
|
||||||
|
load_dotenv(verbose=True)
|
||||||
|
|
||||||
|
DEBUG: bool = os.getenv(key="DEBUG", default="True").lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_dir() -> Path:
|
||||||
|
r"""Get the directory where the application data will be stored.
|
||||||
|
|
||||||
|
This directory is created if it does not exist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path: The directory where the application data will be stored.
|
||||||
|
|
||||||
|
For example, on Windows, it might be:
|
||||||
|
`C:\Users\lovinator\AppData\Roaming\TheLovinator\TTVDrops`
|
||||||
|
|
||||||
|
In this directory, the SQLite database file will be stored as `db.sqlite3`.
|
||||||
|
"""
|
||||||
|
data_dir: str = user_data_dir(
|
||||||
|
appname="TTVDrops",
|
||||||
|
appauthor="TheLovinator",
|
||||||
|
roaming=True,
|
||||||
|
ensure_exists=True,
|
||||||
|
)
|
||||||
|
return Path(data_dir)
|
||||||
|
|
||||||
|
|
||||||
|
DATA_DIR: Path = get_data_dir()
|
||||||
|
|
||||||
|
ADMINS: list[tuple[str, str]] = [("Joakim Hellsén", "tlovinator@gmail.com")]
|
||||||
|
AUTH_USER_MODEL = "core.User"
|
||||||
BASE_DIR: Path = Path(__file__).resolve().parent.parent
|
BASE_DIR: Path = Path(__file__).resolve().parent.parent
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
ROOT_URLCONF = "config.urls"
|
||||||
|
SECRET_KEY: str = os.getenv("DJANGO_SECRET_KEY", default="")
|
||||||
|
|
||||||
|
DEFAULT_FROM_EMAIL: str | None = os.getenv(key="EMAIL_HOST_USER", default=None)
|
||||||
|
EMAIL_HOST: str = os.getenv(key="EMAIL_HOST", default="smtp.gmail.com")
|
||||||
|
EMAIL_HOST_PASSWORD: str | None = os.getenv(key="EMAIL_HOST_PASSWORD", default=None)
|
||||||
|
EMAIL_HOST_USER: str | None = os.getenv(key="EMAIL_HOST_USER", default=None)
|
||||||
|
EMAIL_PORT: int = int(os.getenv(key="EMAIL_PORT", default="587"))
|
||||||
|
EMAIL_SUBJECT_PREFIX = "[TTVDrops] "
|
||||||
|
EMAIL_TIMEOUT: int = int(os.getenv(key="EMAIL_TIMEOUT", default="10"))
|
||||||
|
EMAIL_USE_LOCALTIME = True
|
||||||
|
EMAIL_USE_TLS: bool = os.getenv(key="EMAIL_USE_TLS", default="True").lower() == "true"
|
||||||
|
EMAIL_USE_SSL: bool = os.getenv(key="EMAIL_USE_SSL", default="False").lower() == "true"
|
||||||
|
SERVER_EMAIL: str | None = os.getenv(key="EMAIL_HOST_USER", default=None)
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
LOGOUT_REDIRECT_URL = "/"
|
||||||
|
|
||||||
|
MEDIA_ROOT: Path = DATA_DIR / "media"
|
||||||
|
MEDIA_ROOT.mkdir(exist_ok=True)
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
STATIC_ROOT: Path = BASE_DIR / "staticfiles"
|
||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
STATIC_ROOT.mkdir(exist_ok=True)
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"]
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
TIME_ZONE = "UTC"
|
||||||
SECRET_KEY = "django-insecure-5b+c(lxr3-8o356!qd3_u&x0$)j))56at=&_go+@4gmai-oe2v"
|
WSGI_APPLICATION = "config.wsgi.application"
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
if DEBUG:
|
||||||
DEBUG = True
|
INTERNAL_IPS: list[str] = ["127.0.0.1", "localhost"]
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
if not DEBUG:
|
||||||
|
ALLOWED_HOSTS: list[str] = ["ttvdrops.lovinator.space"]
|
||||||
|
|
||||||
|
LOGGING: dict[str, Any] = {
|
||||||
# Application definition
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "DEBUG",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
"django.utils.autoreload": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
INSTALLED_APPS: list[str] = [
|
INSTALLED_APPS: list[str] = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
|
|
@ -27,6 +108,8 @@ INSTALLED_APPS: list[str] = [
|
||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
|
"django.contrib.sites",
|
||||||
|
"core",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE: list[str] = [
|
MIDDLEWARE: list[str] = [
|
||||||
|
|
@ -36,43 +119,46 @@ MIDDLEWARE: list[str] = [
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "config.urls"
|
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES: list[dict[str, str | list[Path] | bool | dict[str, list[str] | list[tuple[str, list[str]]]]]] = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
"DIRS": [],
|
"DIRS": [BASE_DIR / "templates"],
|
||||||
"APP_DIRS": True,
|
"APP_DIRS": True,
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
"django.template.context_processors.request",
|
|
||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.template.context_processors.i18n",
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "config.wsgi.application"
|
|
||||||
|
|
||||||
|
DATABASES: dict[str, dict[str, str | Path | dict[str, str | int]]] = {
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
"NAME": BASE_DIR / "db.sqlite3",
|
"NAME": DATA_DIR / "db.sqlite3",
|
||||||
}
|
"OPTIONS": {
|
||||||
|
"transaction_mode": "IMMEDIATE",
|
||||||
|
"timeout": 5,
|
||||||
|
"init_command": """
|
||||||
|
PRAGMA journal_mode=WAL;
|
||||||
|
PRAGMA synchronous=NORMAL;
|
||||||
|
PRAGMA mmap_size=134217728;
|
||||||
|
PRAGMA journal_size_limit=27103364;
|
||||||
|
PRAGMA cache_size=2000;
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS: list[dict[str, str]] = [
|
AUTH_PASSWORD_VALIDATORS: list[dict[str, str]] = [
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
|
@ -88,25 +174,15 @@ AUTH_PASSWORD_VALIDATORS: list[dict[str, str]] = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
TESTING: bool = "test" in sys.argv or "PYTEST_VERSION" in os.environ
|
||||||
|
|
||||||
# Internationalization
|
if not TESTING:
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
DEBUG_TOOLBAR_CONFIG: dict[str, str] = {"ROOT_TAG_EXTRA_ATTRS": "hx-preserve"}
|
||||||
|
INSTALLED_APPS = [ # pyright: ignore[reportConstantRedefinition]
|
||||||
LANGUAGE_CODE = "en-us"
|
*INSTALLED_APPS,
|
||||||
|
"debug_toolbar",
|
||||||
TIME_ZONE = "UTC"
|
]
|
||||||
|
MIDDLEWARE = [ # pyright: ignore[reportConstantRedefinition]
|
||||||
USE_I18N = True
|
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||||
|
*MIDDLEWARE,
|
||||||
USE_TZ = True
|
]
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = "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"
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from debug_toolbar.toolbar import debug_toolbar_urls # pyright: ignore[reportMissingTypeStubs]
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
|
@ -11,3 +13,6 @@ if TYPE_CHECKING:
|
||||||
urlpatterns: list[URLResolver] = [
|
urlpatterns: list[URLResolver] = [
|
||||||
path(route="admin/", view=admin.site.urls),
|
path(route="admin/", view=admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if not settings.TESTING:
|
||||||
|
urlpatterns = [*urlpatterns, *debug_toolbar_urls()]
|
||||||
|
|
|
||||||
24
conftest.py
Normal file
24
conftest.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Pytest configuration for Django testing
|
||||||
|
import os
|
||||||
|
|
||||||
|
import django
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import update_last_login
|
||||||
|
from django.contrib.auth.signals import user_logged_in
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure() -> None:
|
||||||
|
"""Configure Django settings for pytest."""
|
||||||
|
if not settings.configured:
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
# Use faster password hasher for tests
|
||||||
|
settings.PASSWORD_HASHERS = [
|
||||||
|
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Disconnect update_last_login signal to avoid unnecessary DB writes
|
||||||
|
user_logged_in.disconnect(update_last_login)
|
||||||
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
9
core/admin.py
Normal file
9
core/admin.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
|
|
||||||
|
# Register your custom User model with the admin
|
||||||
|
admin.site.register(User, UserAdmin)
|
||||||
17
core/apps.py
Normal file
17
core/apps.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
"""Configuration class for the 'core' Django application.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
default_auto_field (str): Specifies the type of auto-created primary key field to use for models in this app.
|
||||||
|
name (str): The full Python path to the application.
|
||||||
|
verbose_name (str): A human-readable name for the application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_auto_field: str = "django.db.models.BigAutoField"
|
||||||
|
name = "core"
|
||||||
|
verbose_name: str = "Core Application"
|
||||||
44
core/migrations/0001_initial.py
Normal file
44
core/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 5.2.4 on 2025-07-08 00:33
|
||||||
|
|
||||||
|
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='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',
|
||||||
|
'db_table': 'auth_user',
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
core/migrations/__init__.py
Normal file
0
core/migrations/__init__.py
Normal file
21
core/models.py
Normal file
21
core/models.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
"""Custom User model extending Django's AbstractUser.
|
||||||
|
|
||||||
|
This allows for future customization of the User model
|
||||||
|
while maintaining all the default Django User functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add any custom fields here in the future
|
||||||
|
# For example:
|
||||||
|
# bio = models.TextField(max_length=500, blank=True)
|
||||||
|
# avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "auth_user" # Keep the same table name as Django's default User
|
||||||
|
verbose_name = "User"
|
||||||
|
verbose_name_plural = "Users"
|
||||||
0
core/tests/__init__.py
Normal file
0
core/tests/__init__.py
Normal file
86
core/tests/test_admin.py
Normal file
86
core/tests/test_admin.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db.models.base import Model
|
||||||
|
from django.test import RequestFactory
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.contrib.admin import ModelAdmin
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db.models.base import Model
|
||||||
|
|
||||||
|
User: type[AbstractUser] = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestUserAdmin:
|
||||||
|
"""Test cases for User admin configuration."""
|
||||||
|
|
||||||
|
def setup_method(self) -> None:
|
||||||
|
"""Set up test data for each test method."""
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.superuser: AbstractUser = User.objects.create_superuser(
|
||||||
|
username="admin",
|
||||||
|
email="admin@example.com",
|
||||||
|
password="test_admin_password_123",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_user_model_registered_in_admin(self) -> None:
|
||||||
|
"""Test that User model is registered in Django admin."""
|
||||||
|
registry: dict[type[Model], ModelAdmin[Any]] = admin.site._registry # noqa: SLF001
|
||||||
|
assert User in registry
|
||||||
|
|
||||||
|
def test_user_admin_class(self) -> None:
|
||||||
|
"""Test that User is registered with UserAdmin."""
|
||||||
|
registry: dict[type[Model], ModelAdmin[Any]] = admin.site._registry # noqa: SLF001
|
||||||
|
admin_class: admin.ModelAdmin[Any] = registry[User]
|
||||||
|
assert isinstance(admin_class, UserAdmin)
|
||||||
|
|
||||||
|
def test_user_admin_can_create_user(self) -> None:
|
||||||
|
"""Test that admin can create users through the interface."""
|
||||||
|
# Test that the admin form can handle user creation
|
||||||
|
registry: dict[type[Model], ModelAdmin[Any]] = admin.site._registry # noqa: SLF001
|
||||||
|
user_admin: admin.ModelAdmin[Any] = registry[User]
|
||||||
|
|
||||||
|
# Check that the admin has the expected methods
|
||||||
|
assert hasattr(user_admin, "get_form")
|
||||||
|
assert hasattr(user_admin, "save_model")
|
||||||
|
|
||||||
|
def test_user_admin_list_display(self) -> None:
|
||||||
|
"""Test admin list display configuration."""
|
||||||
|
registry: dict[type[Model], ModelAdmin[Any]] = admin.site._registry # noqa: SLF001
|
||||||
|
user_admin: admin.ModelAdmin[Any] = registry[User]
|
||||||
|
|
||||||
|
# UserAdmin should have default list_display
|
||||||
|
expected_fields: tuple[str, ...] = (
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"is_staff",
|
||||||
|
)
|
||||||
|
assert user_admin.list_display == expected_fields
|
||||||
|
|
||||||
|
def test_user_admin_search_fields(self) -> None:
|
||||||
|
"""Test admin search fields configuration."""
|
||||||
|
registry: dict[type[Model], ModelAdmin[Any]] = admin.site._registry # noqa: SLF001
|
||||||
|
user_admin: admin.ModelAdmin[Any] = registry[User]
|
||||||
|
|
||||||
|
# UserAdmin should have default search fields
|
||||||
|
expected_search_fields: tuple[str, ...] = ("username", "first_name", "last_name", "email")
|
||||||
|
assert user_admin.search_fields == expected_search_fields
|
||||||
|
|
||||||
|
def test_user_admin_fieldsets(self) -> None:
|
||||||
|
"""Test admin fieldsets configuration."""
|
||||||
|
registry: dict[type[Model], ModelAdmin[Any]] = admin.site._registry # noqa: SLF001
|
||||||
|
user_admin: admin.ModelAdmin[Any] = registry[User]
|
||||||
|
|
||||||
|
# UserAdmin should have fieldsets defined
|
||||||
|
assert hasattr(user_admin, "fieldsets")
|
||||||
|
assert user_admin.fieldsets is not None
|
||||||
178
core/tests/test_models.py
Normal file
178
core/tests/test_models.py
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
|
User: type[AbstractUser] = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestUserModel:
|
||||||
|
"""Test cases for the custom User model."""
|
||||||
|
|
||||||
|
def test_create_user(self) -> None:
|
||||||
|
"""Test creating a regular user."""
|
||||||
|
user: AbstractUser = User.objects.create_user(username="testuser", email="test@example.com", password="testpass123")
|
||||||
|
|
||||||
|
assert user.username == "testuser"
|
||||||
|
assert user.email == "test@example.com"
|
||||||
|
assert user.check_password("testpass123")
|
||||||
|
assert not user.is_staff
|
||||||
|
assert not user.is_superuser
|
||||||
|
assert user.is_active
|
||||||
|
|
||||||
|
def test_create_superuser(self) -> None:
|
||||||
|
"""Test creating a superuser."""
|
||||||
|
superuser: AbstractUser = User.objects.create_superuser(username="admin", email="admin@example.com", password="adminpass123")
|
||||||
|
|
||||||
|
assert superuser.username == "admin"
|
||||||
|
assert superuser.email == "admin@example.com"
|
||||||
|
assert superuser.check_password("adminpass123")
|
||||||
|
assert superuser.is_staff
|
||||||
|
assert superuser.is_superuser
|
||||||
|
assert superuser.is_active
|
||||||
|
|
||||||
|
def test_user_str_representation(self) -> None:
|
||||||
|
"""Test the string representation of the user."""
|
||||||
|
user: AbstractUser = User.objects.create_user(username="testuser", email="test@example.com")
|
||||||
|
|
||||||
|
assert str(user) == "testuser"
|
||||||
|
|
||||||
|
def test_user_email_field(self) -> None:
|
||||||
|
"""Test that email field works correctly."""
|
||||||
|
user: AbstractUser = User.objects.create_user(username="testuser", email="test@example.com", password="testpass123")
|
||||||
|
|
||||||
|
assert user.email == "test@example.com"
|
||||||
|
|
||||||
|
# Test email update
|
||||||
|
user.email = "newemail@example.com"
|
||||||
|
user.save()
|
||||||
|
user.refresh_from_db()
|
||||||
|
|
||||||
|
assert user.email == "newemail@example.com"
|
||||||
|
|
||||||
|
def test_user_unique_username(self) -> None:
|
||||||
|
"""Test that username must be unique."""
|
||||||
|
User.objects.create_user(username="testuser", email="test1@example.com", password="testpass123")
|
||||||
|
|
||||||
|
# Attempting to create another user with the same username should raise an error
|
||||||
|
with pytest.raises(IntegrityError):
|
||||||
|
User.objects.create_user(username="testuser", email="test2@example.com", password="testpass123")
|
||||||
|
|
||||||
|
def test_user_password_hashing(self) -> None:
|
||||||
|
"""Test that passwords are properly hashed."""
|
||||||
|
password = "testpass123"
|
||||||
|
user: AbstractUser = User.objects.create_user(username="testuser", email="test@example.com", password=password)
|
||||||
|
|
||||||
|
# Password should be hashed, not stored in plain text
|
||||||
|
assert user.password != password
|
||||||
|
assert user.check_password(password)
|
||||||
|
assert not user.check_password("wrongpassword")
|
||||||
|
|
||||||
|
def test_user_without_email(self) -> None:
|
||||||
|
"""Test creating a user without email."""
|
||||||
|
user: AbstractUser = User.objects.create_user(username="testuser", password="testpass123")
|
||||||
|
|
||||||
|
assert user.username == "testuser"
|
||||||
|
assert not user.email
|
||||||
|
assert user.check_password("testpass123")
|
||||||
|
|
||||||
|
def test_user_model_meta_options(self) -> None:
|
||||||
|
"""Test the model meta options."""
|
||||||
|
assert User._meta.db_table == "auth_user" # noqa: SLF001
|
||||||
|
assert User._meta.verbose_name == "User" # noqa: SLF001
|
||||||
|
assert User._meta.verbose_name_plural == "Users" # noqa: SLF001
|
||||||
|
|
||||||
|
def test_user_manager_methods(self) -> None:
|
||||||
|
"""Test User manager methods."""
|
||||||
|
# Test create_user method
|
||||||
|
user: AbstractUser = User.objects.create_user(username="regularuser", email="regular@example.com", password="pass123")
|
||||||
|
assert not user.is_staff
|
||||||
|
assert not user.is_superuser
|
||||||
|
|
||||||
|
# Test create_superuser method
|
||||||
|
superuser: AbstractUser = User.objects.create_superuser(username="superuser", email="super@example.com", password="superpass123")
|
||||||
|
assert superuser.is_staff
|
||||||
|
assert superuser.is_superuser
|
||||||
|
|
||||||
|
def test_user_permissions(self) -> None:
|
||||||
|
"""Test user permissions and groups."""
|
||||||
|
user: AbstractUser = User.objects.create_user(username="testuser", email="test@example.com", password="testpass123")
|
||||||
|
|
||||||
|
# Initially user should have no permissions
|
||||||
|
assert not user.user_permissions.exists()
|
||||||
|
assert not user.groups.exists()
|
||||||
|
|
||||||
|
# Test has_perm method (should be False for regular user)
|
||||||
|
assert not user.has_perm("auth.add_user")
|
||||||
|
|
||||||
|
def test_user_active_status(self) -> None:
|
||||||
|
"""Test user active status functionality."""
|
||||||
|
user: AbstractUser = User.objects.create_user(username="testuser", email="test@example.com", password="testpass123")
|
||||||
|
|
||||||
|
# User should be active by default
|
||||||
|
assert user.is_active
|
||||||
|
|
||||||
|
# Test deactivating user
|
||||||
|
user.is_active = False
|
||||||
|
user.save()
|
||||||
|
user.refresh_from_db()
|
||||||
|
|
||||||
|
assert not user.is_active
|
||||||
|
|
||||||
|
def test_user_date_joined(self) -> None:
|
||||||
|
"""Test that date_joined is automatically set."""
|
||||||
|
user: AbstractUser = User.objects.create_user(username="testuser", email="test@example.com", password="testpass123")
|
||||||
|
|
||||||
|
assert user.date_joined is not None
|
||||||
|
|
||||||
|
def test_user_last_login_initially_none(self) -> None:
|
||||||
|
"""Test that last_login is initially None."""
|
||||||
|
user: AbstractUser = User.objects.create_user(username="testuser", email="test@example.com", password="testpass123")
|
||||||
|
|
||||||
|
assert user.last_login is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestUserModelEdgeCases:
|
||||||
|
"""Test edge cases and error conditions for the User model."""
|
||||||
|
|
||||||
|
def test_create_user_without_username(self) -> None:
|
||||||
|
"""Test that creating a user without username raises an error."""
|
||||||
|
with pytest.raises(ValueError, match="The given username must be set"):
|
||||||
|
User.objects.create_user(username="", email="test@example.com", password="testpass123")
|
||||||
|
|
||||||
|
def test_create_superuser_without_is_staff(self) -> None:
|
||||||
|
"""Test that create_superuser enforces is_staff=True."""
|
||||||
|
with pytest.raises(ValueError, match=r"Superuser must have is_staff=True."):
|
||||||
|
User.objects.create_superuser(username="admin", email="admin@example.com", password="adminpass123", is_staff=False)
|
||||||
|
|
||||||
|
def test_create_superuser_without_is_superuser(self) -> None:
|
||||||
|
"""Test that create_superuser enforces is_superuser=True."""
|
||||||
|
with pytest.raises(ValueError, match=r"Superuser must have is_superuser=True."):
|
||||||
|
User.objects.create_superuser(username="admin", email="admin@example.com", password="adminpass123", is_superuser=False)
|
||||||
|
|
||||||
|
def test_user_with_very_long_username(self) -> None:
|
||||||
|
"""Test username length validation."""
|
||||||
|
# Django's default max_length for username is 150
|
||||||
|
long_username: str = "a" * 151
|
||||||
|
|
||||||
|
user = User(username=long_username, email="test@example.com")
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
user.full_clean()
|
||||||
|
|
||||||
|
def test_user_with_invalid_email_format(self) -> None:
|
||||||
|
"""Test email format validation."""
|
||||||
|
user = User(username="testuser", email="invalid-email-format")
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
user.full_clean()
|
||||||
|
|
@ -4,28 +4,30 @@ version = "0.1.0"
|
||||||
description = "Get notified when a new drop is available on Twitch."
|
description = "Get notified when a new drop is available on Twitch."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = ["django>=5.2.4"]
|
dependencies = [
|
||||||
|
"django>=5.2.4",
|
||||||
|
"django-debug-toolbar>=5.2.0",
|
||||||
|
"platformdirs>=4.3.8",
|
||||||
|
"python-dotenv>=1.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = ["pytest>=8.4.1", "pytest-django>=4.11.1"]
|
dev = ["pytest>=8.4.1", "pytest-django>=4.11.1", "pytest-xdist[psutil]>=3.8.0"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
DJANGO_SETTINGS_MODULE = "config.settings"
|
DJANGO_SETTINGS_MODULE = "config.settings"
|
||||||
python_files = ["test_*.py", "*_test.py"]
|
python_files = ["test_*.py", "*_test.py"]
|
||||||
|
addopts = ["-n", "auto", "--reuse-db", "--no-migrations"]
|
||||||
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
lint.select = ["ALL"]
|
lint.select = ["ALL"]
|
||||||
preview = true
|
|
||||||
unsafe-fixes = true
|
|
||||||
fix = true
|
|
||||||
|
|
||||||
# Don't automatically remove unused variables
|
# Don't automatically remove unused variables
|
||||||
lint.unfixable = ["F841"]
|
lint.unfixable = ["F841"]
|
||||||
|
|
||||||
lint.pydocstyle.convention = "google"
|
lint.pydocstyle.convention = "google"
|
||||||
lint.isort.required-imports = ["from __future__ import annotations"]
|
lint.isort.required-imports = ["from __future__ import annotations"]
|
||||||
line-length = 140
|
|
||||||
|
|
||||||
lint.ignore = [
|
lint.ignore = [
|
||||||
"CPY001", # Checks for the absence of copyright notices within Python files.
|
"CPY001", # Checks for the absence of copyright notices within Python files.
|
||||||
|
|
@ -52,7 +54,20 @@ lint.ignore = [
|
||||||
"Q003", # Checks for strings that include escaped quotes, and suggests changing the quote style to avoid the need to escape them.
|
"Q003", # Checks for strings that include escaped quotes, and suggests changing the quote style to avoid the need to escape them.
|
||||||
"W191", # Checks for indentation that uses tabs.
|
"W191", # Checks for indentation that uses tabs.
|
||||||
]
|
]
|
||||||
|
preview = true
|
||||||
|
unsafe-fixes = true
|
||||||
|
fix = true
|
||||||
|
line-length = 140
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"**/tests/**" = ["ARG", "FBT", "PLR2004", "S101", "S311"]
|
"**/tests/**" = [
|
||||||
|
"ARG",
|
||||||
|
"FBT",
|
||||||
|
"PLR2004",
|
||||||
|
"S101",
|
||||||
|
"S311",
|
||||||
|
"S106",
|
||||||
|
"PLR6301",
|
||||||
|
"S105",
|
||||||
|
]
|
||||||
"**/migrations/**" = ["RUF012"]
|
"**/migrations/**" = ["RUF012"]
|
||||||
|
|
|
||||||
85
uv.lock
generated
85
uv.lock
generated
|
|
@ -34,6 +34,28 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/ae/706965237a672434c8b520e89a818e8b047af94e9beb342d0bee405c26c7/django-5.2.4-py3-none-any.whl", hash = "sha256:60c35bd96201b10c6e7a78121bd0da51084733efa303cc19ead021ab179cef5e", size = 8302187, upload-time = "2025-07-02T18:47:35.373Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/ae/706965237a672434c8b520e89a818e8b047af94e9beb342d0bee405c26c7/django-5.2.4-py3-none-any.whl", hash = "sha256:60c35bd96201b10c6e7a78121bd0da51084733efa303cc19ead021ab179cef5e", size = 8302187, upload-time = "2025-07-02T18:47:35.373Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-debug-toolbar"
|
||||||
|
version = "5.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
{ name = "sqlparse" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/9f/97ba2648f66fa208fc7f19d6895586d08bc5f0ab930a1f41032e60f31a41/django_debug_toolbar-5.2.0.tar.gz", hash = "sha256:9e7f0145e1a1b7d78fcc3b53798686170a5b472d9cf085d88121ff823e900821", size = 297901, upload-time = "2025-04-29T05:23:57.533Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/c2/ed3cb815002664349e9e50799b8c00ef15941f4cad797247cadbdeebab02/django_debug_toolbar-5.2.0-py3-none-any.whl", hash = "sha256:15627f4c2836a9099d795e271e38e8cf5204ccd79d5dbcd748f8a6c284dcd195", size = 262834, upload-time = "2025-04-29T05:23:55.472Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "execnet"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
|
@ -52,6 +74,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.3.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|
@ -61,6 +92,21 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psutil"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.19.2"
|
version = "2.19.2"
|
||||||
|
|
@ -98,6 +144,33 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" },
|
{ url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-xdist"
|
||||||
|
version = "3.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "execnet" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
psutil = [
|
||||||
|
{ name = "psutil" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparse"
|
name = "sqlparse"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
@ -113,21 +186,31 @@ version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
|
{ name = "django-debug-toolbar" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-django" },
|
{ name = "pytest-django" },
|
||||||
|
{ name = "pytest-xdist", extra = ["psutil"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [{ name = "django", specifier = ">=5.2.4" }]
|
requires-dist = [
|
||||||
|
{ name = "django", specifier = ">=5.2.4" },
|
||||||
|
{ name = "django-debug-toolbar", specifier = ">=5.2.0" },
|
||||||
|
{ name = "platformdirs", specifier = ">=4.3.8" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pytest", specifier = ">=8.4.1" },
|
{ name = "pytest", specifier = ">=8.4.1" },
|
||||||
{ name = "pytest-django", specifier = ">=4.11.1" },
|
{ name = "pytest-django", specifier = ">=4.11.1" },
|
||||||
|
{ name = "pytest-xdist", extras = ["psutil"], specifier = ">=3.8.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue