Initial commit: 视频主设备管理系统 - 完整DRF后端项目
This commit is contained in:
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
venv/
|
||||
env/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.so
|
||||
media/
|
||||
static/
|
||||
db.sqlite3
|
||||
*.log
|
||||
.env
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
0
config/__init__.py
Normal file
0
config/__init__.py
Normal file
16
config/asgi.py
Normal file
16
config/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for config project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
140
config/settings.py
Normal file
140
config/settings.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Django settings for config project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.0.6.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
SECRET_KEY = 'django-insecure-$6qm&o2(b=hi7snx-dvgcbvbd9p6y%hsk-@=ddv593%r(f)l%%'
|
||||
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework_simplejwt',
|
||||
'django_filters',
|
||||
'drf_yasg',
|
||||
'device_management',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'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'
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = 'zh-hans'
|
||||
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT = BASE_DIR / 'static'
|
||||
|
||||
MEDIA_URL = 'media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
|
||||
],
|
||||
'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,
|
||||
}
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
'SECURITY_DEFINITIONS': {
|
||||
'Bearer': {
|
||||
'type': 'apiKey',
|
||||
'name': 'Authorization',
|
||||
'in': 'header'
|
||||
}
|
||||
},
|
||||
'USE_SESSION_AUTH': True,
|
||||
}
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||
'ROTATE_REFRESH_TOKENS': False,
|
||||
'BLACKLIST_AFTER_ROTATION': False,
|
||||
}
|
||||
40
config/urls.py
Normal file
40
config/urls.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
URL configuration for config project.
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
from rest_framework import permissions
|
||||
from drf_yasg.views import get_schema_view
|
||||
from drf_yasg import openapi
|
||||
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
)
|
||||
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="视频主设备管理系统 API",
|
||||
default_version='v1',
|
||||
description="视频编码器、解码器、矩阵、NVR等设备管理系统API文档",
|
||||
terms_of_service="https://www.example.com/terms/",
|
||||
contact=openapi.Contact(email="contact@example.com"),
|
||||
license=openapi.License(name="BSD License"),
|
||||
),
|
||||
public=True,
|
||||
permission_classes=(permissions.AllowAny,),
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include('device_management.urls')),
|
||||
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path('api-auth/', include('rest_framework.urls')),
|
||||
path('swagger<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
16
config/wsgi.py
Normal file
16
config/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for config project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
0
device_management/__init__.py
Normal file
0
device_management/__init__.py
Normal file
65
device_management/admin.py
Normal file
65
device_management/admin.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from django.contrib import admin
|
||||
from .models import (
|
||||
Device, DeviceSerial, DeviceIP, MaintenanceRecord, DeviceAttachment
|
||||
)
|
||||
|
||||
|
||||
class DeviceSerialInline(admin.TabularInline):
|
||||
model = DeviceSerial
|
||||
extra = 1
|
||||
|
||||
|
||||
class DeviceIPInline(admin.TabularInline):
|
||||
model = DeviceIP
|
||||
extra = 1
|
||||
|
||||
|
||||
class MaintenanceRecordInline(admin.TabularInline):
|
||||
model = MaintenanceRecord
|
||||
extra = 1
|
||||
|
||||
|
||||
class DeviceAttachmentInline(admin.TabularInline):
|
||||
model = DeviceAttachment
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(Device)
|
||||
class DeviceAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'id', 'device_name', 'location', 'building', 'status',
|
||||
'responsible_person', 'enable_date', 'service_duration_days'
|
||||
]
|
||||
list_filter = ['status', 'location', 'building']
|
||||
search_fields = ['device_name', 'location', 'brand', 'model']
|
||||
inlines = [DeviceSerialInline, DeviceIPInline, MaintenanceRecordInline, DeviceAttachmentInline]
|
||||
date_hierarchy = 'enable_date'
|
||||
|
||||
|
||||
@admin.register(DeviceSerial)
|
||||
class DeviceSerialAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'device', 'serial_number', 'serial_type', 'is_primary']
|
||||
list_filter = ['serial_type', 'is_primary']
|
||||
search_fields = ['serial_number']
|
||||
|
||||
|
||||
@admin.register(DeviceIP)
|
||||
class DeviceIPAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'device', 'ip_address', 'ip_type', 'is_primary']
|
||||
list_filter = ['ip_type', 'is_primary']
|
||||
search_fields = ['ip_address']
|
||||
|
||||
|
||||
@admin.register(MaintenanceRecord)
|
||||
class MaintenanceRecordAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'device', 'maintenance_date', 'maintenance_by', 'cost']
|
||||
list_filter = ['maintenance_date', 'maintenance_by']
|
||||
search_fields = ['fault_description', 'repair_content']
|
||||
date_hierarchy = 'maintenance_date'
|
||||
|
||||
|
||||
@admin.register(DeviceAttachment)
|
||||
class DeviceAttachmentAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'device', 'file_name', 'file_type', 'uploaded_at']
|
||||
list_filter = ['file_type', 'uploaded_at']
|
||||
search_fields = ['file_name']
|
||||
6
device_management/apps.py
Normal file
6
device_management/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DeviceManagementConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'device_management'
|
||||
120
device_management/migrations/0001_initial.py
Normal file
120
device_management/migrations/0001_initial.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# Generated by Django 5.2.14 on 2026-05-18 09:21
|
||||
|
||||
import device_management.models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Device',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('location', models.CharField(max_length=100, verbose_name='地点')),
|
||||
('building', models.CharField(blank=True, max_length=50, null=True, verbose_name='楼栋')),
|
||||
('floor', models.CharField(blank=True, max_length=20, null=True, verbose_name='楼层')),
|
||||
('cabinet', models.CharField(blank=True, max_length=50, null=True, verbose_name='机柜')),
|
||||
('device_name', models.CharField(max_length=100, verbose_name='设备名称')),
|
||||
('model', models.CharField(blank=True, max_length=100, null=True, verbose_name='型号')),
|
||||
('brand', models.CharField(blank=True, max_length=100, null=True, verbose_name='品牌')),
|
||||
('mac_address', models.CharField(blank=True, max_length=17, null=True, verbose_name='MAC地址')),
|
||||
('enable_date', models.DateField(blank=True, null=True, verbose_name='启用日期')),
|
||||
('last_inspection_date', models.DateField(blank=True, null=True, verbose_name='最后巡检日期')),
|
||||
('thumbnail', models.ImageField(blank=True, null=True, upload_to=device_management.models.device_thumbnail_path, verbose_name='缩略图')),
|
||||
('status', models.CharField(choices=[('normal', '正常'), ('warning', '告警'), ('offline', '离线'), ('repair', '维修中'), ('scrap', '已报废')], default='normal', max_length=20, verbose_name='状态')),
|
||||
('responsible_person', models.CharField(blank=True, max_length=50, null=True, verbose_name='负责人')),
|
||||
('warranty_expire', models.DateField(blank=True, null=True, verbose_name='保修到期日期')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '设备',
|
||||
'verbose_name_plural': '设备',
|
||||
'db_table': 'device',
|
||||
'indexes': [models.Index(fields=['location', 'building'], name='device_locatio_6a4f0a_idx'), models.Index(fields=['status'], name='device_status_6321a6_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceAttachment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(upload_to=device_management.models.device_attachment_path, verbose_name='文件')),
|
||||
('file_name', models.CharField(max_length=255, verbose_name='文件名')),
|
||||
('file_type', models.CharField(blank=True, max_length=50, null=True, verbose_name='文件类型')),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='上传时间')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='device_management.device', verbose_name='设备')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '设备附件',
|
||||
'verbose_name_plural': '设备附件',
|
||||
'db_table': 'device_attachment',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceIP',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ip_address', models.CharField(max_length=45, unique=True, verbose_name='IP地址')),
|
||||
('ip_type', models.CharField(default='management', max_length=20, verbose_name='IP类型')),
|
||||
('is_primary', models.BooleanField(default=False, verbose_name='是否主IP')),
|
||||
('subnet_mask', models.CharField(blank=True, max_length=15, null=True, verbose_name='子网掩码')),
|
||||
('gateway', models.CharField(blank=True, max_length=45, null=True, verbose_name='网关')),
|
||||
('vlan_id', models.CharField(blank=True, max_length=10, null=True, verbose_name='VLAN ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ips', to='device_management.device', verbose_name='设备')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '设备IP地址',
|
||||
'verbose_name_plural': '设备IP地址',
|
||||
'db_table': 'device_ip',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceSerial',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('serial_number', models.CharField(max_length=100, unique=True, verbose_name='序列号')),
|
||||
('serial_type', models.CharField(default='main', max_length=50, verbose_name='序列号类型')),
|
||||
('is_primary', models.BooleanField(default=False, verbose_name='是否主序列号')),
|
||||
('remark', models.CharField(blank=True, max_length=255, null=True, verbose_name='备注')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='serials', to='device_management.device', verbose_name='设备')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '设备序列号',
|
||||
'verbose_name_plural': '设备序列号',
|
||||
'db_table': 'device_serial',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MaintenanceRecord',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('maintenance_date', models.DateField(verbose_name='维修日期')),
|
||||
('fault_description', models.TextField(blank=True, null=True, verbose_name='故障描述')),
|
||||
('repair_content', models.TextField(blank=True, null=True, verbose_name='维修内容')),
|
||||
('replaced_parts', models.CharField(blank=True, max_length=255, null=True, verbose_name='更换配件')),
|
||||
('maintenance_by', models.CharField(blank=True, max_length=50, null=True, verbose_name='维修人')),
|
||||
('cost', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='费用')),
|
||||
('remark', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='maintenance_records', to='device_management.device', verbose_name='设备')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '维修记录',
|
||||
'verbose_name_plural': '维修记录',
|
||||
'db_table': 'maintenance_record',
|
||||
'ordering': ['-maintenance_date'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
device_management/migrations/__init__.py
Normal file
0
device_management/migrations/__init__.py
Normal file
170
device_management/models.py
Normal file
170
device_management/models.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import os
|
||||
from datetime import date
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from django.core.files.base import ContentFile
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class DeviceStatus(models.TextChoices):
|
||||
NORMAL = 'normal', _('正常')
|
||||
WARNING = 'warning', _('告警')
|
||||
OFFLINE = 'offline', _('离线')
|
||||
REPAIR = 'repair', _('维修中')
|
||||
SCRAP = 'scrap', _('已报废')
|
||||
|
||||
|
||||
def device_attachment_path(instance, filename):
|
||||
return f'devices/{instance.id}/attachments/{filename}'
|
||||
|
||||
|
||||
def device_thumbnail_path(instance, filename):
|
||||
return f'devices/{instance.id}/thumbnails/{filename}'
|
||||
|
||||
|
||||
class Device(models.Model):
|
||||
location = models.CharField(max_length=100, verbose_name='地点')
|
||||
building = models.CharField(max_length=50, blank=True, null=True, verbose_name='楼栋')
|
||||
floor = models.CharField(max_length=20, blank=True, null=True, verbose_name='楼层')
|
||||
cabinet = models.CharField(max_length=50, blank=True, null=True, verbose_name='机柜')
|
||||
device_name = models.CharField(max_length=100, verbose_name='设备名称')
|
||||
model = models.CharField(max_length=100, blank=True, null=True, verbose_name='型号')
|
||||
brand = models.CharField(max_length=100, blank=True, null=True, verbose_name='品牌')
|
||||
mac_address = models.CharField(max_length=17, blank=True, null=True, verbose_name='MAC地址')
|
||||
enable_date = models.DateField(blank=True, null=True, verbose_name='启用日期')
|
||||
last_inspection_date = models.DateField(blank=True, null=True, verbose_name='最后巡检日期')
|
||||
thumbnail = models.ImageField(upload_to=device_thumbnail_path, blank=True, null=True, verbose_name='缩略图')
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=DeviceStatus.choices,
|
||||
default=DeviceStatus.NORMAL,
|
||||
verbose_name='状态'
|
||||
)
|
||||
responsible_person = models.CharField(max_length=50, blank=True, null=True, verbose_name='负责人')
|
||||
warranty_expire = models.DateField(blank=True, null=True, verbose_name='保修到期日期')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'device'
|
||||
verbose_name = '设备'
|
||||
verbose_name_plural = '设备'
|
||||
indexes = [
|
||||
models.Index(fields=['location', 'building']),
|
||||
models.Index(fields=['status']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.device_name} - {self.location}'
|
||||
|
||||
@property
|
||||
def service_duration_days(self):
|
||||
if self.enable_date:
|
||||
return (date.today() - self.enable_date).days
|
||||
return 0
|
||||
|
||||
@property
|
||||
def is_warranty_expired(self):
|
||||
if self.warranty_expire:
|
||||
return date.today() > self.warranty_expire
|
||||
return False
|
||||
|
||||
def generate_thumbnail(self, image_field, size=(200, 200)):
|
||||
if not image_field:
|
||||
return
|
||||
|
||||
try:
|
||||
img = Image.open(image_field)
|
||||
if img.mode in ('RGBA', 'P'):
|
||||
img = img.convert('RGB')
|
||||
|
||||
img.thumbnail(size)
|
||||
thumb_io = BytesIO()
|
||||
img.save(thumb_io, format='JPEG', quality=85)
|
||||
|
||||
thumb_name = f'thumb_{os.path.basename(image_field.name)}'
|
||||
self.thumbnail.save(thumb_name, ContentFile(thumb_io.getvalue()), save=False)
|
||||
logger.info(f'Generated thumbnail for device {self.id}')
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to generate thumbnail: {e}')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class DeviceSerial(models.Model):
|
||||
device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name='serials', verbose_name='设备')
|
||||
serial_number = models.CharField(max_length=100, unique=True, verbose_name='序列号')
|
||||
serial_type = models.CharField(max_length=50, default='main', verbose_name='序列号类型')
|
||||
is_primary = models.BooleanField(default=False, verbose_name='是否主序列号')
|
||||
remark = models.CharField(max_length=255, blank=True, null=True, verbose_name='备注')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'device_serial'
|
||||
verbose_name = '设备序列号'
|
||||
verbose_name_plural = '设备序列号'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.serial_number} ({self.serial_type})'
|
||||
|
||||
|
||||
class DeviceIP(models.Model):
|
||||
device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name='ips', verbose_name='设备')
|
||||
ip_address = models.CharField(max_length=45, unique=True, verbose_name='IP地址')
|
||||
ip_type = models.CharField(max_length=20, default='management', verbose_name='IP类型')
|
||||
is_primary = models.BooleanField(default=False, verbose_name='是否主IP')
|
||||
subnet_mask = models.CharField(max_length=15, blank=True, null=True, verbose_name='子网掩码')
|
||||
gateway = models.CharField(max_length=45, blank=True, null=True, verbose_name='网关')
|
||||
vlan_id = models.CharField(max_length=10, blank=True, null=True, verbose_name='VLAN ID')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'device_ip'
|
||||
verbose_name = '设备IP地址'
|
||||
verbose_name_plural = '设备IP地址'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.ip_address} ({self.ip_type})'
|
||||
|
||||
|
||||
class MaintenanceRecord(models.Model):
|
||||
device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name='maintenance_records', verbose_name='设备')
|
||||
maintenance_date = models.DateField(verbose_name='维修日期')
|
||||
fault_description = models.TextField(blank=True, null=True, verbose_name='故障描述')
|
||||
repair_content = models.TextField(blank=True, null=True, verbose_name='维修内容')
|
||||
replaced_parts = models.CharField(max_length=255, blank=True, null=True, verbose_name='更换配件')
|
||||
maintenance_by = models.CharField(max_length=50, blank=True, null=True, verbose_name='维修人')
|
||||
cost = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, verbose_name='费用')
|
||||
remark = models.TextField(blank=True, null=True, verbose_name='备注')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'maintenance_record'
|
||||
verbose_name = '维修记录'
|
||||
verbose_name_plural = '维修记录'
|
||||
ordering = ['-maintenance_date']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.device.device_name} - {self.maintenance_date}'
|
||||
|
||||
|
||||
class DeviceAttachment(models.Model):
|
||||
device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name='attachments', verbose_name='设备')
|
||||
file = models.FileField(upload_to=device_attachment_path, verbose_name='文件')
|
||||
file_name = models.CharField(max_length=255, verbose_name='文件名')
|
||||
file_type = models.CharField(max_length=50, blank=True, null=True, verbose_name='文件类型')
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name='上传时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'device_attachment'
|
||||
verbose_name = '设备附件'
|
||||
verbose_name_plural = '设备附件'
|
||||
|
||||
def __str__(self):
|
||||
return self.file_name
|
||||
138
device_management/serializers.py
Normal file
138
device_management/serializers.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from rest_framework import serializers
|
||||
from django.db import transaction
|
||||
from .models import (
|
||||
Device, DeviceSerial, DeviceIP, MaintenanceRecord, DeviceAttachment
|
||||
)
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class DeviceSerialSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceSerial
|
||||
fields = ['id', 'serial_number', 'serial_type', 'is_primary', 'remark']
|
||||
|
||||
|
||||
class DeviceIPSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceIP
|
||||
fields = ['id', 'ip_address', 'ip_type', 'is_primary', 'subnet_mask', 'gateway', 'vlan_id']
|
||||
|
||||
|
||||
class MaintenanceRecordSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = MaintenanceRecord
|
||||
fields = ['id', 'maintenance_date', 'fault_description', 'repair_content',
|
||||
'replaced_parts', 'maintenance_by', 'cost', 'remark']
|
||||
|
||||
|
||||
class DeviceAttachmentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceAttachment
|
||||
fields = ['id', 'file', 'file_name', 'file_type', 'uploaded_at']
|
||||
read_only_fields = ['uploaded_at']
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
serials = DeviceSerialSerializer(many=True, required=False)
|
||||
ips = DeviceIPSerializer(many=True, required=False)
|
||||
maintenance_records = MaintenanceRecordSerializer(many=True, read_only=True)
|
||||
attachments = DeviceAttachmentSerializer(many=True, read_only=True)
|
||||
service_duration_days = serializers.IntegerField(read_only=True)
|
||||
is_warranty_expired = serializers.BooleanField(read_only=True)
|
||||
primary_serial = serializers.SerializerMethodField()
|
||||
primary_ip = serializers.SerializerMethodField()
|
||||
latest_maintenance = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'location', 'building', 'floor', 'cabinet', 'device_name',
|
||||
'model', 'brand', 'mac_address', 'enable_date', 'last_inspection_date',
|
||||
'thumbnail', 'status', 'responsible_person', 'warranty_expire',
|
||||
'service_duration_days', 'is_warranty_expired', 'primary_serial',
|
||||
'primary_ip', 'latest_maintenance', 'serials', 'ips',
|
||||
'maintenance_records', 'attachments', 'created_at', 'updated_at']
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
def get_primary_serial(self, obj):
|
||||
primary = obj.serials.filter(is_primary=True).first()
|
||||
if primary:
|
||||
return DeviceSerialSerializer(primary).data
|
||||
return None
|
||||
|
||||
def get_primary_ip(self, obj):
|
||||
primary = obj.ips.filter(is_primary=True).first()
|
||||
if primary:
|
||||
return DeviceIPSerializer(primary).data
|
||||
return None
|
||||
|
||||
def get_latest_maintenance(self, obj):
|
||||
latest = obj.maintenance_records.first()
|
||||
if latest:
|
||||
return MaintenanceRecordSerializer(latest).data
|
||||
return None
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
serials_data = validated_data.pop('serials', [])
|
||||
ips_data = validated_data.pop('ips', [])
|
||||
|
||||
device = Device.objects.create(**validated_data)
|
||||
|
||||
for serial_data in serials_data:
|
||||
DeviceSerial.objects.create(device=device, **serial_data)
|
||||
|
||||
for ip_data in ips_data:
|
||||
DeviceIP.objects.create(device=device, **ip_data)
|
||||
|
||||
logger.info(f'Created device: {device.id} - {device.device_name}')
|
||||
return device
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
serials_data = validated_data.pop('serials', None)
|
||||
ips_data = validated_data.pop('ips', None)
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
if serials_data is not None:
|
||||
instance.serials.all().delete()
|
||||
for serial_data in serials_data:
|
||||
DeviceSerial.objects.create(device=instance, **serial_data)
|
||||
|
||||
if ips_data is not None:
|
||||
instance.ips.all().delete()
|
||||
for ip_data in ips_data:
|
||||
DeviceIP.objects.create(device=instance, **ip_data)
|
||||
|
||||
logger.info(f'Updated device: {instance.id}')
|
||||
return instance
|
||||
|
||||
|
||||
class DeviceListSerializer(serializers.ModelSerializer):
|
||||
primary_serial_number = serializers.SerializerMethodField()
|
||||
primary_ip_address = serializers.SerializerMethodField()
|
||||
latest_maintenance_summary = serializers.SerializerMethodField()
|
||||
service_duration_days = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'location', 'building', 'device_name', 'model', 'brand',
|
||||
'status', 'responsible_person', 'enable_date', 'thumbnail',
|
||||
'primary_serial_number', 'primary_ip_address',
|
||||
'latest_maintenance_summary', 'service_duration_days']
|
||||
|
||||
def get_primary_serial_number(self, obj):
|
||||
primary = obj.serials.filter(is_primary=True).first()
|
||||
return primary.serial_number if primary else None
|
||||
|
||||
def get_primary_ip_address(self, obj):
|
||||
primary = obj.ips.filter(is_primary=True).first()
|
||||
return primary.ip_address if primary else None
|
||||
|
||||
def get_latest_maintenance_summary(self, obj):
|
||||
latest = obj.maintenance_records.first()
|
||||
if latest:
|
||||
return f"{latest.maintenance_date}: {latest.fault_description[:50]}..." if latest.fault_description else str(latest.maintenance_date)
|
||||
return None
|
||||
3
device_management/tests.py
Normal file
3
device_management/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
17
device_management/urls.py
Normal file
17
device_management/urls.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
DeviceViewSet, DeviceSerialViewSet, DeviceIPViewSet,
|
||||
MaintenanceRecordViewSet, DeviceAttachmentViewSet
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'devices', DeviceViewSet, basename='device')
|
||||
router.register(r'serials', DeviceSerialViewSet, basename='deviceserial')
|
||||
router.register(r'ips', DeviceIPViewSet, basename='deviceip')
|
||||
router.register(r'maintenance', MaintenanceRecordViewSet, basename='maintenancerecord')
|
||||
router.register(r'attachments', DeviceAttachmentViewSet, basename='deviceattachment')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
210
device_management/views.py
Normal file
210
device_management/views.py
Normal file
@@ -0,0 +1,210 @@
|
||||
import os
|
||||
from datetime import date
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework import viewsets, status, parsers, renderers
|
||||
from rest_framework.decorators import action, api_view, parser_classes
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated
|
||||
from openpyxl import Workbook, load_workbook
|
||||
from openpyxl.styles import Font
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from .models import (
|
||||
Device, DeviceSerial, DeviceIP, MaintenanceRecord, DeviceAttachment
|
||||
)
|
||||
from .serializers import (
|
||||
DeviceSerializer, DeviceListSerializer, DeviceSerialSerializer,
|
||||
DeviceIPSerializer, MaintenanceRecordSerializer, DeviceAttachmentSerializer
|
||||
)
|
||||
|
||||
|
||||
class DeviceFilter(filters.FilterSet):
|
||||
location = filters.CharFilter(lookup_expr='icontains')
|
||||
building = filters.CharFilter(lookup_expr='icontains')
|
||||
from .models import DeviceStatus
|
||||
status = filters.ChoiceFilter(choices=DeviceStatus.choices)
|
||||
device_name = filters.CharFilter(lookup_expr='icontains')
|
||||
brand = filters.CharFilter(lookup_expr='icontains')
|
||||
enable_date_from = filters.DateFilter(field_name='enable_date', lookup_expr='gte')
|
||||
enable_date_to = filters.DateFilter(field_name='enable_date', lookup_expr='lte')
|
||||
warranty_expire_soon = filters.BooleanFilter(method='filter_warranty_expire_soon')
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['location', 'building', 'status', 'device_name', 'brand']
|
||||
|
||||
def filter_warranty_expire_soon(self, queryset, name, value):
|
||||
if value:
|
||||
from datetime import timedelta
|
||||
soon_date = date.today() + timedelta(days=30)
|
||||
return queryset.filter(warranty_expire__lte=soon_date, warranty_expire__gte=date.today())
|
||||
return queryset
|
||||
|
||||
|
||||
class DeviceViewSet(viewsets.ModelViewSet):
|
||||
queryset = Device.objects.all().prefetch_related('serials', 'ips', 'maintenance_records')
|
||||
serializer_class = DeviceSerializer
|
||||
filterset_class = DeviceFilter
|
||||
search_fields = ['device_name', 'location', 'building', 'brand', 'model']
|
||||
ordering_fields = ['id', 'device_name', 'location', 'enable_date', 'created_at']
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'list':
|
||||
return DeviceListSerializer
|
||||
return DeviceSerializer
|
||||
|
||||
@action(detail=True, methods=['post'], parser_classes=[parsers.MultiPartParser])
|
||||
def upload_attachment(self, request, pk=None):
|
||||
device = self.get_object()
|
||||
file_obj = request.FILES.get('file')
|
||||
|
||||
if not file_obj:
|
||||
return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
file_type = request.data.get('file_type', '')
|
||||
attachment = DeviceAttachment.objects.create(
|
||||
device=device,
|
||||
file=file_obj,
|
||||
file_name=file_obj.name,
|
||||
file_type=file_type
|
||||
)
|
||||
|
||||
if file_type.startswith('image/') or file_obj.name.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp')):
|
||||
device.generate_thumbnail(file_obj)
|
||||
device.save()
|
||||
|
||||
logger.info(f'Uploaded attachment {attachment.id} for device {device.id}')
|
||||
return Response(DeviceAttachmentSerializer(attachment).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def export_excel(self, request):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = '设备列表'
|
||||
|
||||
headers = ['ID', '地点', '楼栋', '设备名称', '型号', '品牌', '状态',
|
||||
'负责人', '启用日期', '主序列号', '主IP', '最近维修简述', '服役天数']
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
for row, device in enumerate(queryset, 2):
|
||||
primary_serial = device.serials.filter(is_primary=True).first()
|
||||
primary_ip = device.ips.filter(is_primary=True).first()
|
||||
latest_maintenance = device.maintenance_records.first()
|
||||
|
||||
ws.cell(row=row, column=1, value=device.id)
|
||||
ws.cell(row=row, column=2, value=device.location)
|
||||
ws.cell(row=row, column=3, value=device.building or '')
|
||||
ws.cell(row=row, column=4, value=device.device_name)
|
||||
ws.cell(row=row, column=5, value=device.model or '')
|
||||
ws.cell(row=row, column=6, value=device.brand or '')
|
||||
ws.cell(row=row, column=7, value=device.get_status_display())
|
||||
ws.cell(row=row, column=8, value=device.responsible_person or '')
|
||||
ws.cell(row=row, column=9, value=str(device.enable_date) if device.enable_date else '')
|
||||
ws.cell(row=row, column=10, value=primary_serial.serial_number if primary_serial else '')
|
||||
ws.cell(row=row, column=11, value=primary_ip.ip_address if primary_ip else '')
|
||||
ws.cell(row=row, column=12, value=str(latest_maintenance) if latest_maintenance else '')
|
||||
ws.cell(row=row, column=13, value=device.service_duration_days)
|
||||
|
||||
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
response['Content-Disposition'] = f'attachment; filename="devices_{date.today()}.xlsx"'
|
||||
wb.save(response)
|
||||
|
||||
logger.info(f'Exported {queryset.count()} devices to Excel')
|
||||
return response
|
||||
|
||||
@action(detail=False, methods=['post'], parser_classes=[parsers.MultiPartParser])
|
||||
def import_excel(self, request):
|
||||
file_obj = request.FILES.get('file')
|
||||
if not file_obj:
|
||||
return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
df = pd.read_excel(file_obj)
|
||||
created_count = 0
|
||||
|
||||
for _, row in df.iterrows():
|
||||
device_data = {
|
||||
'location': str(row.get('地点', '')),
|
||||
'building': str(row.get('楼栋', '')) if pd.notna(row.get('楼栋')) else None,
|
||||
'device_name': str(row.get('设备名称', '')),
|
||||
'model': str(row.get('型号', '')) if pd.notna(row.get('型号')) else None,
|
||||
'brand': str(row.get('品牌', '')) if pd.notna(row.get('品牌')) else None,
|
||||
'status': str(row.get('状态', 'normal')) if pd.notna(row.get('状态')) else 'normal',
|
||||
'responsible_person': str(row.get('负责人', '')) if pd.notna(row.get('负责人')) else None,
|
||||
}
|
||||
|
||||
device = Device.objects.create(**device_data)
|
||||
|
||||
if pd.notna(row.get('主序列号')):
|
||||
DeviceSerial.objects.create(
|
||||
device=device,
|
||||
serial_number=str(row.get('主序列号')),
|
||||
serial_type='main',
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
if pd.notna(row.get('主IP')):
|
||||
DeviceIP.objects.create(
|
||||
device=device,
|
||||
ip_address=str(row.get('主IP')),
|
||||
ip_type='management',
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
created_count += 1
|
||||
|
||||
logger.info(f'Imported {created_count} devices from Excel')
|
||||
return Response({'message': f'成功导入 {created_count} 台设备', 'count': created_count})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Excel import failed: {e}')
|
||||
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class DeviceSerialViewSet(viewsets.ModelViewSet):
|
||||
queryset = DeviceSerial.objects.all()
|
||||
serializer_class = DeviceSerialSerializer
|
||||
search_fields = ['serial_number', 'serial_type']
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
|
||||
class DeviceIPViewSet(viewsets.ModelViewSet):
|
||||
queryset = DeviceIP.objects.all()
|
||||
serializer_class = DeviceIPSerializer
|
||||
search_fields = ['ip_address', 'ip_type']
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
|
||||
class MaintenanceRecordViewSet(viewsets.ModelViewSet):
|
||||
queryset = MaintenanceRecord.objects.all()
|
||||
serializer_class = MaintenanceRecordSerializer
|
||||
filterset_fields = ['device_id', 'maintenance_by']
|
||||
search_fields = ['fault_description', 'repair_content', 'maintenance_by']
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
|
||||
class DeviceAttachmentViewSet(viewsets.ModelViewSet):
|
||||
queryset = DeviceAttachment.objects.all()
|
||||
serializer_class = DeviceAttachmentSerializer
|
||||
parser_classes = [parsers.MultiPartParser, parsers.FormParser]
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
device_id = self.request.data.get('device')
|
||||
device = get_object_or_404(Device, id=device_id)
|
||||
file_obj = self.request.FILES.get('file')
|
||||
|
||||
attachment = serializer.save(
|
||||
device=device,
|
||||
file_name=file_obj.name if file_obj else ''
|
||||
)
|
||||
|
||||
logger.info(f'Created attachment {attachment.id}')
|
||||
22
manage.py
Normal file
22
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')
|
||||
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
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Django==4.2.11
|
||||
djangorestframework==3.15.1
|
||||
djangorestframework-simplejwt==5.3.1
|
||||
drf-yasg==1.21.7
|
||||
Pillow==10.3.0
|
||||
mysqlclient==2.2.4
|
||||
django-filter==24.2
|
||||
openpyxl==3.1.2
|
||||
pandas==2.2.2
|
||||
numpy==1.26.4
|
||||
loguru==0.7.2
|
||||
Reference in New Issue
Block a user