feat: add stage 0 foundation
This commit is contained in:
1
backend/apps/__init__.py
Normal file
1
backend/apps/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
backend/apps/accounts/__init__.py
Normal file
1
backend/apps/accounts/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
3
backend/apps/accounts/api.py
Normal file
3
backend/apps/accounts/api.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from ninja import Router
|
||||
|
||||
router = Router(tags=["accounts"])
|
||||
6
backend/apps/accounts/apps.py
Normal file
6
backend/apps/accounts/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.accounts"
|
||||
1
backend/apps/accounts/management/__init__.py
Normal file
1
backend/apps/accounts/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
backend/apps/accounts/management/commands/__init__.py
Normal file
1
backend/apps/accounts/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from apps.accounts.permissions import DEFAULT_GROUPS, PERMISSIONS
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed MVP permissions and default user groups."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
content_type, _ = ContentType.objects.get_or_create(
|
||||
app_label="system",
|
||||
model="platformpermission",
|
||||
)
|
||||
|
||||
permissions_by_codename: dict[str, Permission] = {}
|
||||
for spec in PERMISSIONS:
|
||||
permission, _ = Permission.objects.get_or_create(
|
||||
content_type=content_type,
|
||||
codename=spec.codename,
|
||||
defaults={"name": spec.name},
|
||||
)
|
||||
if permission.name != spec.name:
|
||||
permission.name = spec.name
|
||||
permission.save(update_fields=["name"])
|
||||
permissions_by_codename[spec.codename] = permission
|
||||
|
||||
for group_name, codenames in DEFAULT_GROUPS.items():
|
||||
group, _ = Group.objects.get_or_create(name=group_name)
|
||||
group.permissions.set(permissions_by_codename[codename] for codename in codenames)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Seeded permissions and groups."))
|
||||
1
backend/apps/accounts/models.py
Normal file
1
backend/apps/accounts/models.py
Normal file
@@ -0,0 +1 @@
|
||||
# Use Django's built-in User, Group, and Permission models for MVP RBAC.
|
||||
61
backend/apps/accounts/permissions.py
Normal file
61
backend/apps/accounts/permissions.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PermissionSpec:
|
||||
codename: str
|
||||
name: str
|
||||
|
||||
|
||||
PERMISSIONS: tuple[PermissionSpec, ...] = (
|
||||
PermissionSpec("ticket.view", "View tickets"),
|
||||
PermissionSpec("ticket.create", "Create tickets"),
|
||||
PermissionSpec("ticket.edit", "Edit tickets"),
|
||||
PermissionSpec("ticket.export", "Export tickets"),
|
||||
PermissionSpec("ticket.assign", "Assign tickets"),
|
||||
PermissionSpec("ticket.close", "Close tickets"),
|
||||
PermissionSpec("knowledge.view", "View knowledge"),
|
||||
PermissionSpec("knowledge.manage", "Manage knowledge"),
|
||||
PermissionSpec("solution.view", "View solutions"),
|
||||
PermissionSpec("solution.manage", "Manage solutions"),
|
||||
PermissionSpec("agent.use", "Use Agents"),
|
||||
PermissionSpec("agent.manage", "Manage Agents"),
|
||||
PermissionSpec("model.manage", "Manage model configs"),
|
||||
PermissionSpec("tool.manage", "Manage MCP tools"),
|
||||
PermissionSpec("audit.view", "View audit logs"),
|
||||
PermissionSpec("system.manage", "Manage system settings"),
|
||||
)
|
||||
|
||||
DEFAULT_GROUPS: dict[str, tuple[str, ...]] = {
|
||||
"System Admin": tuple(permission.codename for permission in PERMISSIONS),
|
||||
"Knowledge Admin": (
|
||||
"ticket.view",
|
||||
"knowledge.view",
|
||||
"knowledge.manage",
|
||||
"solution.view",
|
||||
"solution.manage",
|
||||
"agent.use",
|
||||
),
|
||||
"Support Engineer": (
|
||||
"ticket.view",
|
||||
"ticket.create",
|
||||
"ticket.edit",
|
||||
"ticket.assign",
|
||||
"ticket.close",
|
||||
"knowledge.view",
|
||||
"solution.view",
|
||||
"agent.use",
|
||||
),
|
||||
"Requester": (
|
||||
"ticket.view",
|
||||
"ticket.create",
|
||||
"knowledge.view",
|
||||
"solution.view",
|
||||
),
|
||||
"Auditor": (
|
||||
"ticket.view",
|
||||
"knowledge.view",
|
||||
"solution.view",
|
||||
"audit.view",
|
||||
),
|
||||
}
|
||||
1
backend/apps/accounts/schemas.py
Normal file
1
backend/apps/accounts/schemas.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
backend/apps/accounts/tests/__init__.py
Normal file
1
backend/apps/accounts/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
17
backend/apps/accounts/tests/test_permissions_seed.py
Normal file
17
backend/apps/accounts/tests/test_permissions_seed.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.management import call_command
|
||||
|
||||
from apps.accounts.permissions import DEFAULT_GROUPS, PERMISSIONS
|
||||
|
||||
|
||||
def test_seed_permissions_creates_permissions_and_groups(db):
|
||||
call_command("seed_permissions")
|
||||
|
||||
assert Permission.objects.filter(
|
||||
codename__in=[permission.codename for permission in PERMISSIONS]
|
||||
).count() == len(PERMISSIONS)
|
||||
|
||||
for group_name, codenames in DEFAULT_GROUPS.items():
|
||||
group = Group.objects.get(name=group_name)
|
||||
actual = set(group.permissions.values_list("codename", flat=True))
|
||||
assert actual == set(codenames)
|
||||
1
backend/apps/audit/__init__.py
Normal file
1
backend/apps/audit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
26
backend/apps/audit/api.py
Normal file
26
backend/apps/audit/api.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from ninja import Router, Schema
|
||||
|
||||
from apps.audit.models import OperationAuditLog
|
||||
|
||||
router = Router(tags=["audit"])
|
||||
|
||||
|
||||
class OperationAuditLogOut(Schema):
|
||||
id: int
|
||||
module: str
|
||||
action: str
|
||||
target_type: str
|
||||
target_id: str
|
||||
request_snapshot: dict[str, Any]
|
||||
result: str
|
||||
ip_address: str | None
|
||||
user_agent: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@router.get("/operation-logs", response=list[OperationAuditLogOut])
|
||||
def list_operation_logs(request):
|
||||
return OperationAuditLog.objects.all()[:100]
|
||||
6
backend/apps/audit/apps.py
Normal file
6
backend/apps/audit/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuditConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.audit"
|
||||
61
backend/apps/audit/migrations/0001_initial.py
Normal file
61
backend/apps/audit/migrations/0001_initial.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-30 07:47
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OperationAuditLog',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
('module', models.CharField(max_length=80)),
|
||||
('action', models.CharField(max_length=80)),
|
||||
('target_type', models.CharField(max_length=80)),
|
||||
('target_id', models.CharField(blank=True, max_length=120)),
|
||||
('request_snapshot', models.JSONField(blank=True, default=dict)),
|
||||
('result', models.CharField(max_length=20)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='operation_audit_logs',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [
|
||||
models.Index(fields=['module', 'action'], name='audit_opera_module_8f87af_idx'),
|
||||
models.Index(
|
||||
fields=['target_type', 'target_id'],
|
||||
name='audit_opera_target__560d76_idx',
|
||||
),
|
||||
models.Index(fields=['created_at'], name='audit_opera_created_077b40_idx'),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/apps/audit/migrations/__init__.py
Normal file
0
backend/apps/audit/migrations/__init__.py
Normal file
32
backend/apps/audit/models.py
Normal file
32
backend/apps/audit/models.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class OperationAuditLog(models.Model):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="operation_audit_logs",
|
||||
)
|
||||
module = models.CharField(max_length=80)
|
||||
action = models.CharField(max_length=80)
|
||||
target_type = models.CharField(max_length=80)
|
||||
target_id = models.CharField(max_length=120, blank=True)
|
||||
request_snapshot = models.JSONField(default=dict, blank=True)
|
||||
result = models.CharField(max_length=20)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["module", "action"]),
|
||||
models.Index(fields=["target_type", "target_id"]),
|
||||
models.Index(fields=["created_at"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.module}.{self.action}:{self.result}"
|
||||
1
backend/apps/audit/tests/__init__.py
Normal file
1
backend/apps/audit/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
20
backend/apps/audit/tests/test_audit_api.py
Normal file
20
backend/apps/audit/tests/test_audit_api.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from apps.audit.models import OperationAuditLog
|
||||
|
||||
|
||||
def test_list_operation_logs_returns_recent_logs(client, db):
|
||||
OperationAuditLog.objects.create(
|
||||
module="system",
|
||||
action="health_check",
|
||||
target_type="system",
|
||||
target_id="health",
|
||||
result="success",
|
||||
request_snapshot={"path": "/api/system/health"},
|
||||
)
|
||||
|
||||
response = client.get("/api/audit/operation-logs")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert len(payload) == 1
|
||||
assert payload[0]["module"] == "system"
|
||||
assert payload[0]["action"] == "health_check"
|
||||
1
backend/apps/system/__init__.py
Normal file
1
backend/apps/system/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
8
backend/apps/system/api.py
Normal file
8
backend/apps/system/api.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from ninja import Router
|
||||
|
||||
router = Router(tags=["system"])
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health(request):
|
||||
return {"status": "ok"}
|
||||
6
backend/apps/system/apps.py
Normal file
6
backend/apps/system/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SystemConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.system"
|
||||
1
backend/apps/system/tests/__init__.py
Normal file
1
backend/apps/system/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
5
backend/apps/system/tests/test_health_api.py
Normal file
5
backend/apps/system/tests/test_health_api.py
Normal file
@@ -0,0 +1,5 @@
|
||||
def test_health_endpoint_returns_ok(client):
|
||||
response = client.get("/api/system/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
1
backend/config/__init__.py
Normal file
1
backend/config/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
7
backend/config/asgi.py
Normal file
7
backend/config/asgi.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
||||
|
||||
application = get_asgi_application()
|
||||
9
backend/config/celery.py
Normal file
9
backend/config/celery.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import os
|
||||
|
||||
from celery import Celery
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
||||
|
||||
app = Celery("ai_ticket_platform")
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
app.autodiscover_tasks()
|
||||
1
backend/config/settings/__init__.py
Normal file
1
backend/config/settings/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
80
backend/config/settings/base.py
Normal file
80
backend/config/settings/base.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[3]
|
||||
|
||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-only-change-me")
|
||||
DEBUG = os.getenv("DJANGO_DEBUG", "false").lower() == "true"
|
||||
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"corsheaders",
|
||||
"apps.accounts",
|
||||
"apps.audit",
|
||||
"apps.system",
|
||||
]
|
||||
|
||||
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",
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
ASGI_APPLICATION = "config.asgi.application"
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.getenv("POSTGRES_DB", "ai_ticket_platform"),
|
||||
"USER": os.getenv("POSTGRES_USER", "ai_ticket"),
|
||||
"PASSWORD": os.getenv("POSTGRES_PASSWORD", "ai_ticket"),
|
||||
"HOST": os.getenv("POSTGRES_HOST", "localhost"),
|
||||
"PORT": int(os.getenv("POSTGRES_PORT", "5432")),
|
||||
}
|
||||
}
|
||||
|
||||
LANGUAGE_CODE = "zh-hans"
|
||||
TIME_ZONE = "Asia/Shanghai"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "static/"
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
CORS_ALLOWED_ORIGINS = os.getenv(
|
||||
"CORS_ALLOWED_ORIGINS",
|
||||
"http://localhost:5173",
|
||||
).split(",")
|
||||
|
||||
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
|
||||
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/1")
|
||||
3
backend/config/settings/local.py
Normal file
3
backend/config/settings/local.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .base import * # noqa: F403
|
||||
|
||||
DEBUG = True
|
||||
12
backend/config/settings/test.py
Normal file
12
backend/config/settings/test.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from .base import * # noqa: F403
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": ":memory:",
|
||||
}
|
||||
}
|
||||
|
||||
PASSWORD_HASHERS = [
|
||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||
]
|
||||
15
backend/config/urls.py
Normal file
15
backend/config/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from ninja import NinjaAPI
|
||||
|
||||
from apps.audit.api import router as audit_router
|
||||
from apps.system.api import router as system_router
|
||||
|
||||
api = NinjaAPI(title="AI Ticket Platform API", version="0.1.0")
|
||||
api.add_router("/system", system_router)
|
||||
api.add_router("/audit", audit_router)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/", api.urls),
|
||||
]
|
||||
7
backend/config/wsgi.py
Normal file
7
backend/config/wsgi.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
||||
|
||||
application = get_wsgi_application()
|
||||
14
backend/manage.py
Normal file
14
backend/manage.py
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
52
backend/pyproject.toml
Normal file
52
backend/pyproject.toml
Normal file
@@ -0,0 +1,52 @@
|
||||
[project]
|
||||
name = "ai-ticket-platform-backend"
|
||||
version = "0.1.0"
|
||||
description = "Backend for AI-native technical support ticket platform"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"Django>=5.0,<5.3",
|
||||
"django-ninja>=1.3,<2.0",
|
||||
"psycopg[binary]>=3.2,<4.0",
|
||||
"celery>=5.4,<6.0",
|
||||
"redis>=5.0,<6.0",
|
||||
"django-cors-headers>=4.4,<5.0",
|
||||
"pydantic>=2.8,<3.0",
|
||||
"python-dotenv>=1.0,<2.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=69"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["apps*", "config*"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.2,<9.0",
|
||||
"pytest-django>=4.8,<5.0",
|
||||
"ruff>=0.6,<1.0",
|
||||
"mypy>=1.11,<2.0",
|
||||
"django-stubs[compatible-mypy]>=5.0,<6.0",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
DJANGO_SETTINGS_MODULE = "config.settings.test"
|
||||
python_files = ["test_*.py", "*_test.py"]
|
||||
testpaths = ["apps", "tests"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "B"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
plugins = ["mypy_django_plugin.main"]
|
||||
ignore_missing_imports = true
|
||||
strict_optional = true
|
||||
|
||||
[tool.django-stubs]
|
||||
django_settings_module = "config.settings.test"
|
||||
1
backend/tests/__init__.py
Normal file
1
backend/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
backend/tests/conftest.py
Normal file
1
backend/tests/conftest.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user