2 сентября 2025 г.
Установка
Создаём новое приложение:
mkdir django-articles-api
cd django-articles-api
Структура
Первоначальная структура проекта приложения должна выглядеть так:
django-articles-api/
- .env
- manage.py
- requirements.txt
- config/
- articles/
- models.py
- serializers.py
- views.py
- urls.py
- users/
- models.py
- serializers.py
- views.py
- urls.py
Установка зависимостей
Отредактируем файл (requirements.txt):
Django==4.2.7
djangorestframework==3.14.0
django-cors-headers==4.3.1
django-environ==0.11.2
djangorestframework-simplejwt==5.3.0
PyJWT==2.8.0
Pillow==10.0.1
python-decouple==3.8
Далее выполним следующую команду:
pip install -r requirements.txt
При необходимости установим (обновим):
pip install --upgrade setuptools
Файл .env
Отредактируем файл (.env):
DEBUG=True
SECRET_KEY=your-secret-key-here
DATABASE_URL=sqlite:///db.sqlite3
DB_NAME=django_articles_db
DB_USER=postgres
DB_PASSWORD=password
DB_HOST=localhost
DB_PORT=5432
JWT_SECRET_KEY=your-jwt-secret-key-here
ALLOWED_HOSTS=localhost,127.0.0.1
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
Настройка Django (config/settings.py)
import os
from pathlib import Path
from datetime import timedelta
from decouple import config, Csv
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())
CORS_ALLOWED_ORIGINS = config('CORS_ALLOWED_ORIGINS', cast=Csv())
AUTH_USER_MODEL = 'users.CustomUser'
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'articles',
'users',
'rest_framework_simplejwt.token_blacklist',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'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',
],
},
},
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
),
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=24),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
Если потребуется подключение, например, к PostgreSQL:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST'),
'PORT': config('DB_PORT'),
}
}
Обратите внимание на:
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=24),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
Здесь мы указали:
- ACCESS_TOKEN_LIFETIME: время жизни жизни токена доступа - 24 часа;
- REFRESH_TOKEN_LIFETIME: время жизник refresh токена - 7 дней;
- ROTATE_REFRESH_TOKENS: ротация refresh токена, т.е. каждое обновление токена создаёт новый refresh токен;
- BLACKLIST_AFTER_ROTATION: старый refresh заносится в black list и более не действует;
Файл manage.py
Отредактируем файл:
#!/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', 'config.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()
Модели
Файл (users/models.py):
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
email = models.EmailField(unique=True)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
def __str__(self):
return self.email
Файл (articles/models.py):
from django.db import models
from django.conf import settings
class Article(models.Model):
CATEGORY_CHOICES = [
('technology', 'Technology'),
('science', 'Science'),
('politics', 'Politics'),
('sports', 'Sports'),
('entertainment', 'Entertainment'),
]
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
category = models.CharField(max_length=50, choices=CATEGORY_CHOICES)
image = models.ImageField(upload_to='articles/', blank=True, null=True)
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
Сериализаторы
Файл (users/serializers.py):
from rest_framework import serializers
from django.contrib.auth import authenticate
from .models import CustomUser
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'bio')
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = CustomUser
fields = ('username', 'email', 'password', 'first_name', 'last_name')
def create(self, validated_data):
user = CustomUser.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password'],
first_name=validated_data.get('first_name', ''),
last_name=validated_data.get('last_name', '')
)
return user
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField()
def validate(self, data):
email = data.get('email')
password = data.get('password')
if email and password:
user = authenticate(username=email, password=password)
if not user:
raise serializers.ValidationError('Invalid credentials')
if not user.is_active:
raise serializers.ValidationError('User account is disabled')
data['user'] = user
return data
raise serializers.ValidationError('Must include email and password')
Файл (articles/serializers.py):
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.username', read_only=True)
class Meta:
model = Article
fields = '__all__'
read_only_fields = ('author', 'created_at', 'updated_at')
class ArticleListSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.username', read_only=True)
excerpt = serializers.SerializerMethodField()
class Meta:
model = Article
fields = ('id', 'title', 'author_name', 'category', 'excerpt', 'created_at', 'published')
def get_excerpt(self, obj):
return obj.content[:100] + '...' if len(obj.content) > 100 else obj.content
Представления (Views)
Файл (users/views.py):
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth import login
from .serializers import RegisterSerializer, LoginSerializer, UserSerializer
from .models import CustomUser
@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
serializer = RegisterSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
refresh = RefreshToken.for_user(user)
return Response({
'user': UserSerializer(user).data,
'refresh': str(refresh),
'access': str(refresh.access_token),
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
def login_view(request):
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
user = serializer.validated_data['user']
login(request, user)
refresh = RefreshToken.for_user(user)
return Response({
'user': UserSerializer(user).data,
'refresh': str(refresh),
'access': str(refresh.access_token),
})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET'])
def profile(request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
Файл (articles/views.py):
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated, BasePermission
from django.shortcuts import get_object_or_404
from .models import Article
from .serializers import ArticleSerializer, ArticleListSerializer
class IsAuthorOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
return obj.author == request.user
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
def get_serializer_class(self):
if self.action == 'list':
return ArticleListSerializer
return ArticleSerializer
def get_queryset(self):
queryset = Article.objects.all()
category = self.request.query_params.get('category')
author = self.request.query_params.get('author')
if category:
queryset = queryset.filter(category=category)
if author:
queryset = queryset.filter(author__username=author)
# Для неавторизованных пользователей показываем только опубликованные
if not self.request.user.is_authenticated:
queryset = queryset.filter(published=True)
else:
# Для авторизованных показываем опубликованные + свои неопубликованные
if not self.request.user.is_staff:
queryset = queryset.filter(published=True) | queryset.filter(author=self.request.user)
return queryset
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['patch'], permission_classes=[IsAuthenticated])
def publish(self, request, pk=None):
article = self.get_object()
if article.author != request.user and not request.user.is_staff:
return Response({'error': 'You can only publish your own articles'},
status=status.HTTP_403_FORBIDDEN)
article.published = True
article.save()
return Response({'status': 'article published'})
@action(detail=True, methods=['patch'], permission_classes=[IsAuthenticated])
def unpublish(self, request, pk=None):
article = self.get_object()
if article.author != request.user and not request.user.is_staff:
return Response({'error': 'You can only unpublish your own articles'},
status=status.HTTP_403_FORBIDDEN)
article.published = False
article.save()
return Response({'status': 'article unpublished'})
URLs
Файл (config/urls.py):
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('api/auth/', include('users.urls')),
path('api/articles/', include('articles.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Файл (users/urls.py):
from django.urls import path
from . import views
from rest_framework_simplejwt.views import TokenRefreshView
urlpatterns = [
path('register/', views.register, name='register'),
path('login/', views.login_view, name='login'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('profile/', views.profile, name='profile'),
]
Файл (articles/urls.py):
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'', views.ArticleViewSet, basename='article')
urlpatterns = [
path('', include(router.urls)),
]
Миграции и запуск
Создание миграций:
python manage.py makemigrations users
python manage.py makemigrations articles
Применение миграций:
python manage.py migrate
Создание суперпользователя:
python manage.py createsuperuser
Запуск сервера:
python manage.py runserver
Тестирование API
Регистрация:
curl -X POST http://localhost:8000/api/auth/register/ \
-H "Content-Type: application/json" \
-d '{
"username": "user1",
"email": "user1@jas.ru",
"password": "password123",
"first_name": "Eugene",
"last_name": "Starodubcev"
}'
Логин (получаем оба токена):
curl -X POST http://localhost:8000/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{"email": "user1@jas.ru", "password": "password123"}'
Когда access token истекает, обновляем его:
curl -X POST http://localhost:8000/api/auth/token/refresh/ \
-H "Content-Type: application/json" \
-d '{"refresh":"eyJ..."}'
Получение статей (без авторизации):
curl http://localhost:8000/api/articles/
Получение статей (с авторизацией):
curl http://localhost:8000/api/articles/ \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Получение статей (с фильтрацией по категории):
curl "http://localhost:8000/api/articles/?category=technology"
Создание статьи:
curl -X POST http://localhost:8000/api/articles/ \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Новая статья",
"content": "Содержание статьи...",
"category": "technology",
"published": true
}'
Частичное изменение статьи (PATCH-запрос):
curl -X PATCH http://localhost:8000/api/articles/5/ \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Это совершенно новый заголовок",
"content": "А это обновлённое содержимое статьи...",
"category": "politics"
}'
Полное изменение статьи (PUT-запрос):
curl -X PUT http://localhost:8000/api/articles/5/ \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Полностью новая статья",
"content": "Старое содержимое было полностью стёрто и заменено на это.",
"category": "science",
"published": false
}'
Удаление статьи (DELETE-запрос):
curl -X DELETE http://localhost:8000/api/articles/5/ \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Итого
Мы создали полнофункциональное RESTful API на Django и Django REST Framework (DRF). Основной фокус был сделан на реализации JWT-аутентификации (Bearer Token) и CRUD-операций (Create, Read, Update, Delete) для управления статьями с разграничением прав доступа.