Initial commit: React + Django full-stack project setup
- Backend: Django 4.2 + DRF + JWT + GraphQL - Frontend: React 18 + MobX + styled-components - Deployment: Docker + Docker Compose + Nginx - Database: PostgreSQL support - Documentation: README, INIT, PROJECT_DOCS, TESTING
This commit is contained in:
45
backend/.gitignore
vendored
Normal file
45
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Django
|
||||
*.log
|
||||
/local_settings.py
|
||||
/staticfiles/
|
||||
/media/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
31
backend/Dockerfile
Normal file
31
backend/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
-y --no-install-recommends && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy project
|
||||
COPY . .
|
||||
|
||||
# Collect static files
|
||||
RUN python manage.py collectstatic --noinput --settings=config.settings.prod
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run gunicorn
|
||||
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
|
||||
1
backend/apps/__init__.py
Normal file
1
backend/apps/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Apps package
|
||||
1
backend/apps/api/__init__.py
Normal file
1
backend/apps/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API app
|
||||
6
backend/apps/api/apps.py
Normal file
6
backend/apps/api/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.api'
|
||||
7
backend/apps/api/graphql_urls.py
Normal file
7
backend/apps/api/graphql_urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from graphene_django.views import GraphQLView
|
||||
from apps.api.schema import schema
|
||||
|
||||
urlpatterns = [
|
||||
path('', GraphQLView.as_view(graphiql=True, schema=schema)),
|
||||
]
|
||||
31
backend/apps/api/schema.py
Normal file
31
backend/apps/api/schema.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import graphene
|
||||
from graphene_django import DjangoObjectType
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
class UserType(DjangoObjectType):
|
||||
"""GraphQL type for User."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
"""Root GraphQL Query."""
|
||||
|
||||
all_users = graphene.List(UserType)
|
||||
me = graphene.Field(UserType)
|
||||
|
||||
def resolve_all_users(root, info):
|
||||
"""Resolve all users query."""
|
||||
return User.objects.all()
|
||||
|
||||
def resolve_me(root, info):
|
||||
"""Resolve current user query."""
|
||||
if info.context.user.is_authenticated:
|
||||
return info.context.user
|
||||
return None
|
||||
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
29
backend/apps/api/serializers.py
Normal file
29
backend/apps/api/serializers.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
|
||||
"""Custom JWT token serializer that includes user data."""
|
||||
|
||||
@classmethod
|
||||
def get_token(cls, user):
|
||||
token = super().get_token(user)
|
||||
# Add custom claims
|
||||
token['email'] = user.email
|
||||
token['username'] = user.username
|
||||
return token
|
||||
|
||||
def validate(self, attrs):
|
||||
data = super().validate(attrs)
|
||||
# Add user data to response
|
||||
data['user'] = {
|
||||
'id': self.user.id,
|
||||
'email': self.user.email,
|
||||
'username': self.user.username,
|
||||
'first_name': self.user.first_name,
|
||||
'last_name': self.user.last_name,
|
||||
}
|
||||
return data
|
||||
12
backend/apps/api/urls.py
Normal file
12
backend/apps/api/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenRefreshView,
|
||||
)
|
||||
from apps.users.urls import urlpatterns as users_urls
|
||||
from apps.api.views import CustomTokenObtainPairView
|
||||
|
||||
urlpatterns = [
|
||||
path('auth/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path('', include(users_urls)),
|
||||
]
|
||||
11
backend/apps/api/views.py
Normal file
11
backend/apps/api/views.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
from .serializers import CustomTokenObtainPairSerializer
|
||||
|
||||
|
||||
class CustomTokenObtainPairView(TokenObtainPairView):
|
||||
"""Custom token view that returns user data with tokens."""
|
||||
|
||||
serializer_class = CustomTokenObtainPairSerializer
|
||||
1
backend/apps/core/__init__.py
Normal file
1
backend/apps/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Core app
|
||||
6
backend/apps/core/apps.py
Normal file
6
backend/apps/core/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.core'
|
||||
1
backend/apps/users/__init__.py
Normal file
1
backend/apps/users/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Users app
|
||||
6
backend/apps/users/apps.py
Normal file
6
backend/apps/users/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.users'
|
||||
22
backend/apps/users/models.py
Normal file
22
backend/apps/users/models.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model extending AbstractUser."""
|
||||
|
||||
email = models.EmailField(unique=True)
|
||||
first_name = models.CharField(max_length=150)
|
||||
last_name = models.CharField(max_length=150)
|
||||
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username', 'first_name']
|
||||
|
||||
class Meta:
|
||||
db_table = 'users'
|
||||
verbose_name = 'User'
|
||||
verbose_name_plural = 'Users'
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
20
backend/apps/users/serializers.py
Normal file
20
backend/apps/users/serializers.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from rest_framework import serializers
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for User model."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'avatar')
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
class UserDetailSerializer(serializers.ModelSerializer):
|
||||
"""Detailed serializer for User model."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
read_only_fields = ('id', 'date_joined', 'last_login')
|
||||
10
backend/apps/users/urls.py
Normal file
10
backend/apps/users/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import UserViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'users', UserViewSet, basename='user')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
23
backend/apps/users/views.py
Normal file
23
backend/apps/users/views.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from rest_framework import viewsets, permissions
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from .models import User
|
||||
from .serializers import UserSerializer, UserDetailSerializer
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet for User model."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action in ['retrieve', 'update', 'partial_update']:
|
||||
return UserDetailSerializer
|
||||
return UserSerializer
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def me(self, request):
|
||||
"""Get current user."""
|
||||
serializer = self.get_serializer(request.user)
|
||||
return Response(serializer.data)
|
||||
1
backend/config/__init__.py
Normal file
1
backend/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Config package
|
||||
9
backend/config/asgi.py
Normal file
9
backend/config/asgi.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
ASGI config for the project.
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.prod')
|
||||
application = get_asgi_application()
|
||||
165
backend/config/settings/base.py
Normal file
165
backend/config/settings/base.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Base Django settings for the project.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Build paths inside the project
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-change-this-in-production')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# Third party
|
||||
'rest_framework',
|
||||
'rest_framework_simplejwt',
|
||||
'corsheaders',
|
||||
'django_filters',
|
||||
'graphene_django',
|
||||
|
||||
# Local apps
|
||||
'apps.users',
|
||||
'apps.core',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'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',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
|
||||
# Database
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.environ.get('DB_NAME', 'postgres'),
|
||||
'USER': os.environ.get('DB_USER', 'postgres'),
|
||||
'PASSWORD': os.environ.get('DB_PASSWORD', 'postgres'),
|
||||
'HOST': os.environ.get('DB_HOST', 'localhost'),
|
||||
'PORT': os.environ.get('DB_PORT', '5432'),
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
|
||||
# Password validation
|
||||
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
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
TIME_ZONE = 'UTC'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
|
||||
# Media files
|
||||
MEDIA_URL = 'media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Default primary key field type
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# REST Framework
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
),
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
'rest_framework.filters.SearchFilter',
|
||||
'rest_framework.filters.OrderingFilter',
|
||||
),
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
}
|
||||
|
||||
# JWT Settings
|
||||
from datetime import timedelta
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||
'ROTATE_REFRESH_TOKENS': True,
|
||||
'BLACKLIST_AFTER_ROTATION': True,
|
||||
'UPDATE_LAST_LOGIN': True,
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': SECRET_KEY,
|
||||
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||
}
|
||||
|
||||
# CORS Settings
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
]
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
]
|
||||
|
||||
# GraphQL
|
||||
GRAPHENE = {
|
||||
'SCHEMA': 'apps.api.schema.schema',
|
||||
'MIDDLEWARE': [
|
||||
'graphql_jwt.middleware.JSONWebTokenMiddleware',
|
||||
],
|
||||
}
|
||||
19
backend/config/settings/dev.py
Normal file
19
backend/config/settings/dev.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Development settings for the project.
|
||||
"""
|
||||
|
||||
from .base import *
|
||||
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0']
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
# Email backend (console for development)
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# Additional apps for development
|
||||
INSTALLED_APPS += [
|
||||
'django_extensions',
|
||||
]
|
||||
31
backend/config/settings/prod.py
Normal file
31
backend/config/settings/prod.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Production settings for the project.
|
||||
"""
|
||||
|
||||
from .base import *
|
||||
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
|
||||
|
||||
# Security settings
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# CORS - use environment variable
|
||||
CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', '').split(',')
|
||||
CSORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Email backend (use your email service in production)
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = os.environ.get('EMAIL_HOST')
|
||||
EMAIL_PORT = os.environ.get('EMAIL_PORT', 587)
|
||||
EMAIL_USE_TLS = True
|
||||
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
|
||||
18
backend/config/urls.py
Normal file
18
backend/config/urls.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
URL configuration for the project.
|
||||
"""
|
||||
|
||||
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/', include('apps.api.urls')),
|
||||
path('graphql/', include('apps.api.graphql_urls')),
|
||||
]
|
||||
|
||||
# Serve media files in development
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
9
backend/config/wsgi.py
Normal file
9
backend/config/wsgi.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
WSGI config for the project.
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.prod')
|
||||
application = get_wsgi_application()
|
||||
22
backend/manage.py
Normal file
22
backend/manage.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/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.dev')
|
||||
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()
|
||||
11
backend/requirements.txt
Normal file
11
backend/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Django>=4.2,<5.0
|
||||
djangorestframework>=3.14
|
||||
djangorestframework-simplejwt>=5.2
|
||||
django-cors-headers>=4.0
|
||||
psycopg2-binary>=2.9
|
||||
python-dotenv>=1.0
|
||||
Pillow>=10.0
|
||||
graphene-django>=3.1
|
||||
django-filter>=23.0
|
||||
gunicorn>=21.0
|
||||
whitenoise>=6.5
|
||||
29
backend/start.sh
Executable file
29
backend/start.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 Starting Django Backend..."
|
||||
|
||||
# 激活虚拟环境
|
||||
if [ -d "venv" ]; then
|
||||
source venv/bin/activate
|
||||
echo "✅ Virtual environment activated"
|
||||
else
|
||||
echo "❌ Virtual environment not found. Creating one..."
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
# 安装依赖
|
||||
echo "📦 Installing dependencies..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 运行迁移
|
||||
echo "🔄 Running migrations..."
|
||||
python manage.py migrate
|
||||
|
||||
# 收集静态文件
|
||||
echo "📁 Collecting static files..."
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
# 启动服务器
|
||||
echo "🎉 Starting development server on http://localhost:8000"
|
||||
python manage.py runserver
|
||||
Reference in New Issue
Block a user