Python Coding Standards
This guide establishes the conventions and standards that the backend development team must follow to maintain consistency and quality in Python code, following PEP 8 and community best practices.
Naming Conventions
Variables and Functions
Use snake_case for variables and functions:
# ✅ Correct
user_name = 'john_doe'
user_age = 25
def calculate_total_price(items):
"""Calculates the total price of a list of items."""
return sum(item.price for item in items)
# ❌ Incorrect
userName = 'john_doe'
UserAge = 25
def calculateTotalPrice(items):
return sum(item.price for item in items)
Use descriptive and meaningful names:
# ✅ Correct
active_user_count = 150
async def fetch_user_profile(user_id: int) -> User:
"""Gets a user's profile by their ID."""
pass
# ❌ Incorrect
count = 150
async def fetch(id: int):
pass
Classes
Use PascalCase for class names:
# ✅ Correct
class UserService:
def __init__(self):
pass
class EmailNotificationHandler:
def send_email(self):
pass
# ❌ Incorrect
class userService:
def __init__(self):
pass
class email_notification_handler:
def send_email(self):
pass
Constants
Use SCREAMING_SNAKE_CASE for constants:
# ✅ Correct
MAX_RETRY_ATTEMPTS = 3
API_BASE_URL = 'https://api.example.com'
DEFAULT_TIMEOUT = 5000
# ❌ Incorrect
maxRetryAttempts = 3
apiBaseUrl = 'https://api.example.com'
Modules and Packages
Use snake_case for module and package names:
# ✅ Correct
import user_service
from email_handlers import notification_sender
# ❌ Incorrect
import UserService
from EmailHandlers import NotificationSender
Private Variables
Use underscore prefix to indicate privacy:
# ✅ Correct
class UserService:
def __init__(self):
self._private_var = "private"
self.__very_private = "very private"
def _private_method(self):
"""Private method for internal use."""
pass
def public_method(self):
"""Public API method."""
pass
# ❌ Incorrect
class UserService:
def __init__(self):
self.privateVar = "private"
def privateMethod(self):
pass
File and Directory Organization
Directory Structure
Django Backend Project Structure with Laravel-like Organization:
This structure follows Django's modular, application-based architecture where each feature is organized into separate Django apps, promoting separation of concerns and reusability.
project_name/
├── manage.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── entrypoint.sh
├── project_name/ # Main Django project configuration
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── core/ # Core shared utilities and models
│ ├── config/
│ │ ├── __init__.py
│ │ └── apps.py
│ ├── http/
│ │ ├── controllers/
│ │ │ └── __init__.py
│ │ └── services/
│ │ └── __init__.py
│ ├── models/
│ │ └── __init__.py
│ ├── serializers/
│ │ └── __init__.py
│ ├── migrations/
│ │ └── __init__.py
│ ├── tests/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ └── views.py
├── auth/ # Authentication and authorization
│ ├── config/
│ │ ├── __init__.py
│ │ └── apps.py
│ ├── http/
│ │ ├── controllers/
│ │ │ └── __init__.py
│ │ └── services/
│ │ └── __init__.py
│ ├── models/
│ │ └── __init__.py
│ ├── serializers/
│ │ └── __init__.py
│ ├── migrations/
│ │ └── __init__.py
│ ├── tests/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ └── views.py
├── api/ # API endpoints and routing
│ ├── config/
│ │ ├── __init__.py
│ │ └── apps.py
│ ├── http/
│ │ └── controllers/
│ │ └── __init__.py
│ ├── models/
│ │ └── __init__.py
│ ├── migrations/
│ │ └── __init__.py
│ ├── tests/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ └── views.py
├── feature_app_name/ # Feature-specific apps (products, orders, notifications, etc.)
│ ├── config/
│ │ ├── __init__.py
│ │ └── apps.py
│ ├── http/
│ │ ├── controllers/
│ │ │ └── __init__.py
│ │ └── services/
│ │ └── __init__.py
│ ├── models/
│ │ └── __init__.py
│ ├── serializers/
│ │ └── __init__.py
│ ├── migrations/
│ │ └── __init__.py
│ ├── tests/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ └── views.py
└── static/ # Static files
Key Principles:
- Modular Architecture: Each major feature is a separate Django app
- Laravel-like Structure:
http/controllers/andhttp/services/for better organization - Separation of Concerns: Clear distinction between models, controllers, services, and serializers
- Django + DRF: Built exclusively with Django and Django REST Framework
- Scalable: Easy to add new feature apps following the same pattern
File Naming Conventions
Use PascalCase for Controller, Model, Service, and Serializer files:
# ✅ Correct - Controllers
SomeController.py
# ✅ Correct - Models
SomeModel.py
# ✅ Correct - Services
SomeService.py
# ✅ Correct - Serializers
SomeSerializer.py
# ✅ Correct - Test Cases
SomeTestCase.py
Standard Django App Structure:
app_name/
├── config/ # App configuration
├── http/
│ ├── controllers/ # PascalCase controller files
│ └── services/ # PascalCase service files
├── models/ # PascalCase model files
├── serializers/ # PascalCase serializer files
├── migrations/ # Django migrations
├── tests/ # PascalCase test files
├── admin.py # Django admin configuration
├── apps.py # Django app configuration
├── urls.py # URL patterns
└── views.py # Django views
Internal File Organization
Import order (PEP 8):
# 1. Standard library imports
import os
import sys
from datetime import datetime
from typing import List, Optional
# 2. Third-party imports
import fastapi
import sqlalchemy
from pydantic import BaseModel
# 3. Local/internal imports
from core.services.user_service import UserService
from infrastructure.repositories.user_repository import UserRepository
from shared.exceptions.validation_error import ValidationError
Class element order:
class UserService:
"""Service for user management."""
# 1. Class attributes
MAX_LOGIN_ATTEMPTS = 3
def __init__(self, user_repository: UserRepository):
"""Initialize the user service."""
# 2. Instance attributes
self._user_repository = user_repository
self._logger = logging.getLogger(__name__)
# 3. Public methods
async def create_user(self, user_data: CreateUserDto) -> User:
"""Create a new user in the system."""
pass
def get_user_by_id(self, user_id: int) -> Optional[User]:
"""Get a user by their ID."""
pass
# 4. Private methods
def _validate_user_data(self, user_data: CreateUserDto) -> bool:
"""Validate user data."""
pass
def _hash_password(self, password: str) -> str:
"""Generate password hash."""
pass
Package Structure
Use __init__.py to define public API:
# src/core/services/__init__.py
"""Domain services."""
from .user_service import UserService
from .email_service import EmailService
__all__ = ['UserService', 'EmailService']
Documentation and Comments
Docstrings
Use docstrings for functions, methods, and classes (PEP 257):
def create_user(user_data: CreateUserDto, options: Optional[CreateUserOptions] = None) -> User:
"""
Create a new user in the system.
Args:
user_data: User data to create
options: Additional options for creation
Returns:
Created user with assigned ID
Raises:
ValidationError: When user data is invalid
DuplicateEmailError: When email already exists
Example:
>>> user_data = CreateUserDto(email="test@example.com", name="Test")
>>> user = create_user(user_data)
>>> print(user.id)
1
"""
# Implementation...
Use docstrings for classes:
class UserService:
"""
Service for system user management.
This service handles all user-related operations,
including creation, authentication, and profile management.
Attributes:
_user_repository: Repository for user data access
_logger: Logger for event recording
Example:
>>> user_service = UserService(user_repository)
>>> user = await user_service.create_user(user_data)
"""
def __init__(self, user_repository: UserRepository):
"""
Initialize the user service.
Args:
user_repository: Repository for data access
"""
self._user_repository = user_repository
Use inline comments for complex logic:
# Apply password hash using bcrypt with configurable salt rounds
salt_rounds = config.get('BCRYPT_SALT_ROUNDS', 10)
hashed_password = bcrypt.hashpw(user_data.password.encode('utf-8'),
bcrypt.gensalt(rounds=salt_rounds))
# Verify that email is not already registered before creating user
existing_user = await self._user_repository.find_by_email(user_data.email)
if existing_user:
raise DuplicateEmailError('Email already registered')
Avoid obvious comments:
# ❌ Incorrect - obvious comment
user_id = user.id # Get the user ID
# ✅ Correct - no unnecessary comment
user_id = user.id
# ✅ Correct - useful comment
user_id = user.id # Use ID for change auditing
API Documentation
Use docstrings for FastAPI endpoints:
@app.post("/api/users", response_model=User)
async def create_user(user_data: CreateUserDto) -> User:
"""
Create a new user.
Creates a new user in the system with the provided data.
Args:
user_data: User data to create
Returns:
Created user with assigned ID
Raises:
HTTPException: 400 if data is invalid
HTTPException: 409 if email already exists
"""
return await user_service.create_user(user_data)
README and Module Documentation
Each main module should have a README.md:
# User Service Module
## Description
Module responsible for system user management.
## Components
- `UserController`: Handles HTTP requests related to users
- `UserService`: Contains business logic for users
- `UserRepository`: User data access
## Usage
```python
from core.services.user_service import UserService
from infrastructure.repositories.user_repository import UserRepository
user_repository = UserRepository()
user_service = UserService(user_repository)
new_user = await user_service.create_user(user_data)
Dependencies
- bcrypt: For password hashing
- PyJWT: For JWT token generation
- SQLAlchemy: For database ORM
### TODO and FIXME Comments
**Use standard format for pending tasks:**
```python
# TODO: Implement more robust email validation
# FIXME: Handle edge case when user doesn't have permissions
# HACK: Temporary solution until new auth system is implemented
# NOTE: This method will be deprecated in version 2.0
Code Best Practices
Functions and Methods
Keep functions small and with single responsibility:
# ✅ Correct - function with single responsibility
def validate_email(email: str) -> bool:
"""Validate email format using regex."""
import re
email_regex = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'$'
return re.match(email_regex, email) is not None
async def hash_password(password: str) -> str:
"""Generate password hash using bcrypt."""
import bcrypt
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
# ❌ Incorrect - function with multiple responsibilities
async def process_user(user_data: dict) -> dict:
"""Process user data (does too many things)."""
# Validate email
if '@' not in user_data['email']:
raise ValueError('Invalid email')
# Hash password
user_data['password'] = await hash_password(user_data['password'])
# Save to database
return await database.save(user_data)
Error Handling
Use specific exception classes:
# ✅ Correct
class ValidationError(Exception):
"""Data validation error."""
def __init__(self, message: str, field: str):
super().__init__(message)
self.field = field
class NotFoundError(Exception):
"""Error when a resource is not found."""
def __init__(self, resource: str, identifier: str):
message = f"{resource} with id {identifier} not found"
super().__init__(message)
self.resource = resource
self.identifier = identifier
# Usage
if not user:
raise NotFoundError('User', str(user_id))
Using Type Hints
Define explicit types using typing:
from typing import List, Optional, Dict, Any
from pydantic import BaseModel
# ✅ Correct
class CreateUserDto(BaseModel):
"""DTO for user creation."""
email: str
password: str
first_name: str
last_name: str
role: Optional[str] = None
async def create_user(user_data: CreateUserDto) -> User:
"""Create a new user."""
pass
def get_users_by_role(role: str) -> List[User]:
"""Get users by role."""
pass
# ❌ Incorrect
async def create_user(user_data): # No type hints
pass
Use Enums for constant values:
from enum import Enum
# ✅ Correct
class UserRole(str, Enum):
"""Available user roles."""
ADMIN = 'admin'
USER = 'user'
MODERATOR = 'moderator'
class HttpStatus(int, Enum):
"""HTTP status codes."""
OK = 200
CREATED = 201
BAD_REQUEST = 400
UNAUTHORIZED = 401
NOT_FOUND = 404
INTERNAL_SERVER_ERROR = 500
# Usage
user = User(role=UserRole.ADMIN)
Using Dataclasses and Pydantic
Use dataclasses for simple data structures:
from dataclasses import dataclass
from datetime import datetime
@dataclass
class User:
"""User entity."""
id: int
email: str
first_name: str
last_name: str
created_at: datetime
is_active: bool = True
Use Pydantic for validation and serialization:
from pydantic import BaseModel, EmailStr, validator
class CreateUserRequest(BaseModel):
"""Request to create user."""
email: EmailStr
password: str
first_name: str
last_name: str
@validator('password')
def validate_password(cls, v):
"""Validate that password has at least 8 characters."""
if len(v) < 8:
raise ValueError('Password must be at least 8 characters')
return v
Linting and Formatting
Flake8 Configuration
Install linting tools:
pip install flake8 flake8-docstrings flake8-import-order flake8-bugbear
pip install mypy # For type checking
.flake8 configuration:
[flake8]
max-line-length = 88
extend-ignore =
E203, # whitespace before ':'
E501, # line too long (handled by black)
W503, # line break before binary operator
D100, # Missing docstring in public module
D104, # Missing docstring in public package
exclude =
.git,
__pycache__,
.venv,
venv,
migrations,
.pytest_cache,
build,
dist
per-file-ignores =
__init__.py:F401,D104
tests/*:D100,D101,D102,D103,D104
*/settings/*:D100,D101,D102,D103
*/migrations/*:D100,D101,D102,D103,D104
docstring-convention = google
import-order-style = google
application-import-names = core,auth,api
Black Configuration
Install Black:
pip install black
pyproject.toml configuration:
[tool.black]
line-length = 88
target-version = ['py39', 'py310', 'py311']
include = '\.pyi?$'$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| migrations
)/
'''
Pre-commit Configuration
Install pre-commit:
pip install pre-commit
.pre-commit-config.yaml configuration:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflict
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
hooks:
- id: mypy
additional_dependencies: [django-stubs, djangorestframework-stubs]
Install hooks:
pre-commit install
Django-Specific Standards
Django Project Structure
Application organization:
project_name/
├── manage.py
├── requirements.txt
├── core/ # Main configuration and shared utilities
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # Base configuration
│ │ ├── development.py # Development configuration
│ │ ├── production.py # Production configuration
│ │ └── testing.py # Test configuration
│ ├── urls.py
│ ├── wsgi.py
│ └── utils/ # Shared utilities
├── auth/ # Authentication and authorization
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ └── tests/
├── api/ # API endpoints
│ ├── v1/
│ │ ├── urls.py
│ │ └── views.py
│ └── middleware/
└── apps/ # Feature-specific applications
├── products/
├── orders/
└── notifications/
Django Models
Model conventions:
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _
class User(AbstractUser):
"""
Custom user model.
Extends Django's default user model to add
business-specific additional fields.
"""
email = models.EmailField(_('email address'), unique=True)
first_name = models.CharField(_('first name'), max_length=150)
last_name = models.CharField(_('last name'), max_length=150)
is_verified = models.BooleanField(_('is verified'), default=False)
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
updated_at = models.DateTimeField(_('updated at'), auto_now=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name', 'last_name']
class Meta:
verbose_name = _('User')
verbose_name_plural = _('Users')
db_table = 'auth_user'
def __str__(self) -> str:
"""String representation of the user."""
return f"{self.first_name} {self.last_name} ({self.email})"
def get_full_name(self) -> str:
"""Return the user's full name."""
return f"{self.first_name} {self.last_name}".strip()
Use Abstract Base Models for common functionality:
class TimestampedModel(models.Model):
"""
Abstract model that provides timestamp fields.
All models that inherit from this class will automatically have
created_at and updated_at fields.
"""
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
updated_at = models.DateTimeField(_('updated at'), auto_now=True)
class Meta:
abstract = True
class Product(TimestampedModel):
"""Product model."""
name = models.CharField(_('name'), max_length=200)
description = models.TextField(_('description'), blank=True)
price = models.DecimalField(_('price'), max_digits=10, decimal_places=2)
is_active = models.BooleanField(_('is active'), default=True)
class Meta:
verbose_name = _('Product')
verbose_name_plural = _('Products')
ordering = ['-created_at']
def __str__(self) -> str:
return self.name
Django REST Framework Serializers
Serializer conventions:
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Product
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
"""
Serializer for the User model.
Provides complete user serialization including
read-only fields like created_at and updated_at.
"""
full_name = serializers.CharField(source='get_full_name', read_only=True)
class Meta:
model = User
fields = [
'id', 'email', 'first_name', 'last_name', 'full_name',
'is_verified', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'is_verified']
class CreateUserSerializer(serializers.ModelSerializer):
"""
Serializer for creating new users.
Includes password validation and password confirmation.
"""
password = serializers.CharField(write_only=True, min_length=8)
password_confirm = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['email', 'first_name', 'last_name', 'password', 'password_confirm']
def validate(self, attrs):
"""Validate that passwords match."""
if attrs['password'] != attrs['password_confirm']:
raise serializers.ValidationError("Passwords don't match")
return attrs
def create(self, validated_data):
"""Create a new user with hashed password."""
validated_data.pop('password_confirm')
user = User.objects.create_user(**validated_data)
return user
class ProductSerializer(serializers.ModelSerializer):
"""Serializer for the Product model."""
class Meta:
model = Product
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at']
def validate_price(self, value):
"""Validate that price is positive."""
if value <= 0:
raise serializers.ValidationError("Price must be positive")
return value
Views and ViewSets
Use DRF ViewSets for RESTful APIs:
from rest_framework import viewsets, status, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django.contrib.auth import get_user_model
from .models import Product
from .serializers import UserSerializer, CreateUserSerializer, ProductSerializer
User = get_user_model()
class UserViewSet(viewsets.ModelViewSet):
"""
ViewSet for user management.
Provides complete CRUD operations for users with
appropriate permissions and action-specific serializers.
"""
queryset = User.objects.all()
permission_classes = [permissions.IsAuthenticated]
def get_serializer_class(self):
"""Return the appropriate serializer based on action."""
if self.action == 'create':
return CreateUserSerializer
return UserSerializer
def get_permissions(self):
"""Return action-specific permissions."""
if self.action == 'create':
# Allow public registration
return [permissions.AllowAny()]
return super().get_permissions()
@action(detail=False, methods=['get'])
def me(self, request):
"""Endpoint to get current user information."""
serializer = self.get_serializer(request.user)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def verify_email(self, request, pk=None):
"""Endpoint to verify user email."""
user = self.get_object()
# Verification logic here
user.is_verified = True
user.save()
return Response({'status': 'email verified'})
class ProductViewSet(viewsets.ModelViewSet):
"""ViewSet for product management."""
queryset = Product.objects.filter(is_active=True)
serializer_class = ProductSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get_queryset(self):
"""Filter products based on query parameters."""
queryset = super().get_queryset()
name = self.request.query_params.get('name')
if name:
queryset = queryset.filter(name__icontains=name)
return queryset
def perform_destroy(self, instance):
"""Soft delete: mark as inactive instead of deleting."""
instance.is_active = False
instance.save()
URLs and Routing
URL organization:
# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('api.v1.urls')),
path('auth/', include('auth.urls')),
]
# api/v1/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from auth.views import UserViewSet
from apps.products.views import ProductViewSet
router = DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'products', ProductViewSet)
urlpatterns = [
path('', include(router.urls)),
]
Django Settings
Configuration organization:
# core/settings/base.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Application configuration
DJANGO_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
THIRD_PARTY_APPS = [
'rest_framework',
'corsheaders',
]
LOCAL_APPS = [
'core',
'auth',
'api',
'apps.products',
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# Django REST Framework configuration
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
# Database configuration
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME', 'project_db'),
'USER': os.getenv('DB_USER', 'postgres'),
'PASSWORD': os.getenv('DB_PASSWORD', ''),
'HOST': os.getenv('DB_HOST', 'localhost'),
'PORT': os.getenv('DB_PORT', '5432'),
}
}
# Internationalization configuration
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Custom user model
AUTH_USER_MODEL = 'auth.User'
Django Tests
Test structure and conventions:
# auth/tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
User = get_user_model()
class UserModelTest(TestCase):
"""Tests for the User model."""
def setUp(self):
"""Initial setup for each test."""
self.user_data = {
'email': 'test@example.com',
'first_name': 'Test',
'last_name': 'User',
'password': 'testpass123'
}
def test_create_user_success(self):
"""Test successful user creation."""
user = User.objects.create_user(**self.user_data)
self.assertEqual(user.email, self.user_data['email'])
self.assertEqual(user.first_name, self.user_data['first_name'])
self.assertTrue(user.check_password(self.user_data['password']))
self.assertFalse(user.is_verified)
def test_user_str_representation(self):
"""Test user string representation."""
user = User.objects.create_user(**self.user_data)
expected = f"{user.first_name} {user.last_name} ({user.email})"
self.assertEqual(str(user), expected)
def test_get_full_name(self):
"""Test get_full_name method."""
user = User.objects.create_user(**self.user_data)
expected = f"{user.first_name} {user.last_name}"
self.assertEqual(user.get_full_name(), expected)
# auth/tests/test_views.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.contrib.auth import get_user_model
from django.urls import reverse
User = get_user_model()
class UserViewSetTest(APITestCase):
"""Tests for UserViewSet."""
def setUp(self):
"""Initial setup for each test."""
self.user = User.objects.create_user(
email='test@example.com',
first_name='Test',
last_name='User',
password='testpass123'
)
self.client.force_authenticate(user=self.user)
def test_get_current_user(self):
"""Test endpoint to get current user."""
url = reverse('user-me')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['email'], self.user.email)ta['email'], self.user.email)
def test_create_user_success(self):
"""Test successful user creation."""
self.client.force_authenticate(user=None) # No authentication
url = reverse('user-list')
data = {
'email': 'newuser@example.com',
'first_name': 'New',
'last_name': 'User',
'password': 'newpass123',
'password_confirm': 'newpass123'
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(User.objects.filter(email=data['email']).exists())
Custom Management Commands
Create management commands:
# core/management/commands/setup_dev_data.py
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from apps.products.models import Product
User = get_user_model()
class Command(BaseCommand):
"""
Command to set up development data.
Usage: python manage.py setup_dev_data
"""
help = 'Set up initial development data'
def add_arguments(self, parser):
"""Add arguments to the command."""
parser.add_argument(
'--users',
type=int,
default=10,
help='Number of users to create'
)
def handle(self, *args, **options):
"""Execute the command."""
users_count = options['users']
self.stdout.write('Creating development data...')
# Create superuser if it doesn't exist
if not User.objects.filter(is_superuser=True).exists():
User.objects.create_superuser(
email='admin@example.com',
first_name='Admin',
last_name='User',
password='admin123'
)
self.stdout.write(
self.style.SUCCESS('Superuser created: admin@example.com')
)
# Create test users
for i in range(users_count):
User.objects.get_or_create(
email=f'user{i}@example.com',
defaults={
'first_name': f'User{i}',
'last_name': 'Test',
'password': 'testpass123'
}
)
self.stdout.write(
self.style.SUCCESS('Development data configured successfully')
)
This configuration ensures that all Django code follows the framework's best practices and maintains consistency with the standards established by the team.