From 2ca8b8d181cdac43508240dfe09e9b100b288300 Mon Sep 17 00:00:00 2001 From: xiaji Date: Fri, 5 Dec 2025 13:45:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E4=B8=AD=E5=BF=83=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=9A=84=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trae/documents/任务中心管理系统实现计划.md | 200 ++++++++++++++++++ db.sqlite3 | Bin 0 -> 155648 bytes manage.py | 22 ++ task_center/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 171 bytes .../__pycache__/settings.cpython-311.pyc | Bin 0 -> 2813 bytes task_center/__pycache__/urls.cpython-311.pyc | Bin 0 -> 1542 bytes task_center/__pycache__/wsgi.cpython-311.pyc | Bin 0 -> 701 bytes task_center/asgi.py | 16 ++ task_center/settings.py | 139 ++++++++++++ task_center/urls.py | 28 +++ task_center/wsgi.py | 16 ++ tasks/__init__.py | 0 tasks/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 165 bytes tasks/__pycache__/admin.cpython-311.pyc | Bin 0 -> 220 bytes tasks/__pycache__/apps.cpython-311.pyc | Bin 0 -> 534 bytes tasks/__pycache__/models.cpython-311.pyc | Bin 0 -> 4751 bytes tasks/__pycache__/serializers.cpython-311.pyc | Bin 0 -> 3243 bytes tasks/__pycache__/urls.cpython-311.pyc | Bin 0 -> 1462 bytes tasks/__pycache__/views.cpython-311.pyc | Bin 0 -> 6821 bytes .../views_frontend.cpython-311.pyc | Bin 0 -> 3359 bytes tasks/admin.py | 3 + tasks/apps.py | 6 + tasks/management/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 176 bytes tasks/management/commands/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 185 bytes .../commands/check_task_timeouts.py | 37 ++++ tasks/migrations/0001_initial.py | 65 ++++++ tasks/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 4101 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 176 bytes tasks/models.py | 60 ++++++ tasks/serializers.py | 30 +++ tasks/templates/tasks/base.html | 39 ++++ tasks/templates/tasks/client_create.html | 25 +++ tasks/templates/tasks/client_list.html | 37 ++++ tasks/templates/tasks/index.html | 96 +++++++++ tasks/templates/tasks/task_create.html | 37 ++++ tasks/templates/tasks/task_detail.html | 98 +++++++++ tasks/templates/tasks/task_list.html | 48 +++++ tasks/tests.py | 3 + tasks/tests/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 171 bytes .../test_api.cpython-311-pytest-8.3.2.pyc | Bin 0 -> 8363 bytes .../__pycache__/test_api.cpython-311.pyc | Bin 0 -> 8227 bytes ...est_factories.cpython-311-pytest-8.3.2.pyc | Bin 0 -> 3345 bytes .../test_factories.cpython-311.pyc | Bin 0 -> 3210 bytes ...t_integration.cpython-311-pytest-8.3.2.pyc | Bin 0 -> 5871 bytes .../test_integration.cpython-311.pyc | Bin 0 -> 5735 bytes .../test_models.cpython-311-pytest-8.3.2.pyc | Bin 0 -> 6962 bytes .../__pycache__/test_models.cpython-311.pyc | Bin 0 -> 6763 bytes .../test_views.cpython-311-pytest-8.3.2.pyc | Bin 0 -> 4529 bytes .../__pycache__/test_views.cpython-311.pyc | Bin 0 -> 4393 bytes tasks/tests/test_api.py | 128 +++++++++++ tasks/tests/test_factories.py | 38 ++++ tasks/tests/test_integration.py | 89 ++++++++ tasks/tests/test_models.py | 109 ++++++++++ tasks/tests/test_views.py | 60 ++++++ tasks/urls.py | 23 ++ tasks/views.py | 105 +++++++++ tasks/views_frontend.py | 54 +++++ 62 files changed, 1611 insertions(+) create mode 100644 .trae/documents/任务中心管理系统实现计划.md create mode 100644 db.sqlite3 create mode 100644 manage.py create mode 100644 task_center/__init__.py create mode 100644 task_center/__pycache__/__init__.cpython-311.pyc create mode 100644 task_center/__pycache__/settings.cpython-311.pyc create mode 100644 task_center/__pycache__/urls.cpython-311.pyc create mode 100644 task_center/__pycache__/wsgi.cpython-311.pyc create mode 100644 task_center/asgi.py create mode 100644 task_center/settings.py create mode 100644 task_center/urls.py create mode 100644 task_center/wsgi.py create mode 100644 tasks/__init__.py create mode 100644 tasks/__pycache__/__init__.cpython-311.pyc create mode 100644 tasks/__pycache__/admin.cpython-311.pyc create mode 100644 tasks/__pycache__/apps.cpython-311.pyc create mode 100644 tasks/__pycache__/models.cpython-311.pyc create mode 100644 tasks/__pycache__/serializers.cpython-311.pyc create mode 100644 tasks/__pycache__/urls.cpython-311.pyc create mode 100644 tasks/__pycache__/views.cpython-311.pyc create mode 100644 tasks/__pycache__/views_frontend.cpython-311.pyc create mode 100644 tasks/admin.py create mode 100644 tasks/apps.py create mode 100644 tasks/management/__init__.py create mode 100644 tasks/management/__pycache__/__init__.cpython-311.pyc create mode 100644 tasks/management/commands/__init__.py create mode 100644 tasks/management/commands/__pycache__/__init__.cpython-311.pyc create mode 100644 tasks/management/commands/check_task_timeouts.py create mode 100644 tasks/migrations/0001_initial.py create mode 100644 tasks/migrations/__init__.py create mode 100644 tasks/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 tasks/migrations/__pycache__/__init__.cpython-311.pyc create mode 100644 tasks/models.py create mode 100644 tasks/serializers.py create mode 100644 tasks/templates/tasks/base.html create mode 100644 tasks/templates/tasks/client_create.html create mode 100644 tasks/templates/tasks/client_list.html create mode 100644 tasks/templates/tasks/index.html create mode 100644 tasks/templates/tasks/task_create.html create mode 100644 tasks/templates/tasks/task_detail.html create mode 100644 tasks/templates/tasks/task_list.html create mode 100644 tasks/tests.py create mode 100644 tasks/tests/__init__.py create mode 100644 tasks/tests/__pycache__/__init__.cpython-311.pyc create mode 100644 tasks/tests/__pycache__/test_api.cpython-311-pytest-8.3.2.pyc create mode 100644 tasks/tests/__pycache__/test_api.cpython-311.pyc create mode 100644 tasks/tests/__pycache__/test_factories.cpython-311-pytest-8.3.2.pyc create mode 100644 tasks/tests/__pycache__/test_factories.cpython-311.pyc create mode 100644 tasks/tests/__pycache__/test_integration.cpython-311-pytest-8.3.2.pyc create mode 100644 tasks/tests/__pycache__/test_integration.cpython-311.pyc create mode 100644 tasks/tests/__pycache__/test_models.cpython-311-pytest-8.3.2.pyc create mode 100644 tasks/tests/__pycache__/test_models.cpython-311.pyc create mode 100644 tasks/tests/__pycache__/test_views.cpython-311-pytest-8.3.2.pyc create mode 100644 tasks/tests/__pycache__/test_views.cpython-311.pyc create mode 100644 tasks/tests/test_api.py create mode 100644 tasks/tests/test_factories.py create mode 100644 tasks/tests/test_integration.py create mode 100644 tasks/tests/test_models.py create mode 100644 tasks/tests/test_views.py create mode 100644 tasks/urls.py create mode 100644 tasks/views.py create mode 100644 tasks/views_frontend.py diff --git a/.trae/documents/任务中心管理系统实现计划.md b/.trae/documents/任务中心管理系统实现计划.md new file mode 100644 index 0000000..6178948 --- /dev/null +++ b/.trae/documents/任务中心管理系统实现计划.md @@ -0,0 +1,200 @@ +# 任务中心管理系统最终实现计划 + +## 项目结构设计 +1. 创建Django项目:`task_center` +2. 创建任务管理应用:`tasks` +3. 配置数据库为SQLite +4. 集成Bootstrap前端框架 +5. 配置pytest测试框架 + +## 数据库模型设计 + +### Client模型 +| 字段名 | 类型 | 描述 | +|-------|------|------| +| id | AutoField | 客户端ID | +| name | CharField(unique=True) | 客户端标识 | +| token | CharField(max_length=128) | API Token | +| last_seen | DateTimeField | 最后活跃时间 | +| created_at | DateTimeField | 创建时间 | + +### Task模型 +| 字段名 | 类型 | 描述 | +|-------|------|------| +| id | AutoField | 任务ID | +| name | CharField | 任务名称 | +| client_name | CharField(null=True, blank=True) | 指定执行客户端 | +| script | TextField(null=True, blank=True) | 执行脚本 | +| status | CharField(choices=STATUS_CHOICES, default='pending') | 任务状态 | +| timeout_seconds | IntegerField(default=259200) | 超时时间(默认3天=259200秒) | +| created_at | DateTimeField | 创建时间 | +| updated_at | DateTimeField | 更新时间 | +| assigned_to | CharField(null=True, blank=True) | 实际执行客户端 | +| started_at | DateTimeField(null=True, blank=True) | 开始执行时间 | +| completed_at | DateTimeField(null=True, blank=True) | 完成时间 | + +### TaskResult模型 +| 字段名 | 类型 | 描述 | +|-------|------|------| +| id | AutoField | 结果ID | +| task | ForeignKey(Task) | 关联任务 | +| client | ForeignKey(Client) | 执行客户端 | +| result_file | FileField(upload_to='task_results/') | 结果文件 | +| status | CharField(choices=STATUS_CHOICES) | 执行状态 | +| message | TextField(null=True, blank=True) | 执行消息 | +| created_at | DateTimeField | 创建时间 | + +## 状态定义 +```python +STATUS_CHOICES = [ + ('pending', '待分配'), + ('assigned', '已分配'), + ('running', '执行中'), + ('success', '成功'), + ('failed', '失败'), + ('retrying', '重试中'), + ('timeout', '超时,关闭'), +] +``` + +## API接口设计 + +### 认证机制 +- **所有API端点均需token认证** +- 客户端通过HTTP头`Authorization: Token `进行身份验证 +- 使用Django REST Framework的TokenAuthentication +- 未提供有效token的请求将返回401 Unauthorized + +### 任务管理API +- `GET /api/tasks/` - 获取任务列表(需认证) +- `GET /api/tasks//` - 获取任务详情(需认证) +- `POST /api/tasks/` - 创建任务(需认证) +- `PUT /api/tasks//` - 更新任务(需认证) +- `DELETE /api/tasks//` - 删除任务(需认证) + +### 客户端API +- `POST /api/tasks/claim/` - 客户端原子认领任务(需认证) +- `POST /api/tasks//start/` - 客户端开始执行任务(需认证) +- `POST /api/tasks//complete/` - 客户端完成任务(需认证) +- `POST /api/task_results/` - 上传任务结果(需认证,支持文件上传) + +### 客户端管理API +- `GET /api/clients/` - 获取客户端列表(需认证) +- `POST /api/clients/` - 创建客户端(需认证) +- `GET /api/clients//` - 获取客户端详情(需认证) + +### 文件下载API +- `GET /api/task_results//download/` - 下载任务结果文件(需认证) + +## 文件上传实现细节 + +### 1. Django媒体文件配置 +```python +# settings.py +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +``` + +### 2. 文件上传API实现 +- 使用Django REST Framework的`MultiPartParser`和`FormParser` +- API端点:`POST /api/task_results/` +- 支持`multipart/form-data`格式上传文件 +- 上传字段:`task_id`、`status`、`message`、`result_file` + +### 3. 前端文件上传实现 +- 使用HTML5的`input type="file"`元素 +- 表单设置`enctype="multipart/form-data"` +- 使用Bootstrap样式美化文件上传控件 +- 支持进度条显示(可选) + +### 4. 文件存储策略 +- 本地文件系统存储:`media/task_results/`目录 +- 文件名自动生成,避免冲突 +- 支持大文件上传(通过Django默认配置) + +### 5. 文件下载实现 +- API端点:`GET /api/task_results//download/` +- 返回`Content-Disposition: attachment`头,触发浏览器下载 +- 支持断点续传(通过Django默认配置) + +## 前端页面设计 +1. 任务列表页:展示所有任务,支持筛选和搜索 +2. 任务创建页:表单创建新任务 +3. 任务详情页:查看任务详情和执行结果历史 +4. 客户端管理页:管理客户端列表 +5. 结果文件上传页:支持文件上传和状态更新 +6. 结果文件下载功能:点击即可下载 + +## 核心功能实现 + +### 1. 全API Token认证 +- 所有API视图均使用`TokenAuthentication` +- 配置`DEFAULT_AUTHENTICATION_CLASSES`和`DEFAULT_PERMISSION_CLASSES` + +### 2. 原子任务认领机制 +- 使用数据库事务确保任务认领的原子性 +- 客户端调用`/api/tasks/claim/`时,系统自动查找并分配可用任务 + +### 3. 任务超时自动回收 +- 使用Django管理命令定期检查超时任务 +- 超过`timeout_seconds`的任务自动设置为`timeout`状态 +- 管理命令:`python manage.py check_task_timeouts` + +### 4. 任务结果版本管理 +- TaskResult模型记录每次执行结果 +- 支持查看任务的完整执行历史 +- 支持下载不同版本的结果文件 + +## 测试用例设计 + +### 目录结构 +``` +task_center/ +├── tasks/ +│ ├── tests/ +│ │ ├── __init__.py +│ │ ├── test_models.py +│ │ ├── test_views.py +│ │ ├── test_api.py +│ │ ├── test_integration.py +│ │ └── test_factories.py +│ └── ... +├── conftest.py +└── pytest.ini +``` + +### 测试类型 +1. **模型测试**:测试模型字段、方法和关系 +2. **API测试**:测试所有API端点的功能和认证机制 +3. **文件上传测试**:测试文件上传和下载功能 +4. **视图测试**:测试前端视图渲染 +5. **集成测试**:测试完整的任务流程 +6. **工厂测试**:使用factory_boy创建测试数据 + +## 实现步骤 +1. 创建Django项目和应用 +2. 配置项目设置(数据库、REST Framework、认证、媒体文件等) +3. 实现数据库模型 +4. 实现API视图和序列化器,包括文件上传功能 +5. 配置URL路由 +6. 实现前端模板,包括文件上传表单 +7. 实现任务超时管理命令 +8. 编写测试用例 +9. 测试API接口和功能 + +## 技术栈 +- 后端:Django 5.0.6 + Django REST Framework +- 前端:HTML + Bootstrap 5 +- 数据库:SQLite +- 文件存储:本地文件系统 +- 测试:pytest + factory_boy + +## 预期效果 +- 所有API端点均需要token认证 +- 支持结果文件的上传和下载功能 +- 后台可通过admin或API创建和管理任务 +- 客户端通过API进行身份验证,原子认领任务 +- 支持任务超时自动回收 +- 完整的任务执行历史记录 +- 简单大方的Bootstrap前端界面 +- 全面的测试用例覆盖 \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..f1847546fb1ac561def2fbae552d31e2757cf560 GIT binary patch literal 155648 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 JfqhTl{{Zt0&ocl3 literal 0 HcmV?d00001 diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..0bcddb2 --- /dev/null +++ b/manage.py @@ -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', 'task_center.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() diff --git a/task_center/__init__.py b/task_center/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_center/__pycache__/__init__.cpython-311.pyc b/task_center/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a3788e44021f9988489ceeb702fb53afe300cc09 GIT binary patch literal 171 zcmZ3^%ge<81V@97GC}lX5CH>>P{wCAAY(d13PUi1CZpd&=*nD(9QqvOc2+`Mya?%pU613j4%!D6W zl8V?5vFiNrL>3`p-=>o2#G!d z-q-Mro-bpQUbuY(gRc)K?!_nFqn?<+Q89^QVycJ4bPwV9SCmLYn}Bu(C&df~4vt}; zM}I{r6rTAa)}zEM&WJgj#W|9|$DA32aQ=&kLW#%lEG~f5EIuC4xqzNfs9sFW<9Trw zpA-vV?>KyOctJdYPl@w*5x>y`Z1E&MEiM4!n?9m=3jaVr#kb^(uuQsgPu6<|V-ed{ z^q$3R8zy7R)_tiaTFBYBHe*Wmd{En53Z1_D`!5vuq|J`eErJGgAYGGd+~CA<(;Li z8$bH6|MA6Z3iwDaT|eKy&|4YY=oC}V+z5v48oF&NTWk`t2=mJf4H$9{G!~~dY}lBtza5sFGl^wO+or6MUBkTZ#{IOgib-dAtfVOz zt7KO;2^%b5vG-WPj{|h-hdk%qX&Ym&*pR+3nhR+aUp4{>pcy(_4x0ZVYMI+?8OR-l z?1n_IPx&H4Ov8R~Fotj{-5@_hiqngjL@=yeA4N5!b?p_4P?hfedvfP4#;3Lf<WZ#t1Fc5%p1pIV|Kr!yAC*gM?Bfx@%Ig9&1{yJy?#l>q zqAw3DH@eX&J4v_#plnLcn`2mrq-J2ETI{OQgR(N#6ry5xvBIx$8?}zq;@kDAAi&N^ zanVgm5?mLO9(6%KMTCI7P zuSmC=LPv0O)rQc4DVTtq6I{AptyF6KU9Qc$nRc_;kv7`3as4RARxNXWdb;Tf*WrA#g1f+NbZ$v4SoGayyCU7;YE@u1A*-`BuCczst@Bd3S>XY& zTIVIP2|1YB0EerWZZzD4-|L9rc?XcozIC@Si7FsBrL`(wtGKx~FLb1}Hsmgtx$UOw ze5J|-;`FHNJO4-Yy?>+q>wO@%-rIcf&7UKYsDc-#!2H zH!uG3*=DH5HYe9F+u!?l>I2wqxYur)%Yfwnf3kLfdQpm^ya-LjJmixFM4ftq7Q(NW zK$-l{-~Dy!7dL-(^KUa}{yKB!>CDnI|2_X~=F%|b1LihaWu6pY!6;JP7(E8V>{)QTo`Eyy!(EpqvT@t?=NZ@Sr^! zoXDPz!1}$jXm(+kJ;=-s(+4xhhN%NsT*Aws+(~erM)OynpaNjV&+Mm`L30U=#tIQw aX%1!Qhw1(7iHFIj*|&zY|1&Q@#Qy`tZmDVj literal 0 HcmV?d00001 diff --git a/task_center/__pycache__/urls.cpython-311.pyc b/task_center/__pycache__/urls.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f1ee799515793c921c7e0ea78737c102cf964f5 GIT binary patch literal 1542 zcmb7E&x<2P6s}IH^P_t+aYUR&6lz$UOk}oO7ZxS{jP4FQpf0StAR*AxlddEkyStjI zj_%B9P!EHrIV`%~^fD^$9tC9&{tF4@5c(j3Goqd%ix*G6>P}-uR?tdy@$%}`_rCYl z>;9rr(GZN??F;Vn7@?o!pKKK8#{J*G_yjqqg&f7nd5WKFy}XZG*jHO>@*jHz zzt}1!w(6Dqa;u!!1yA$!mad>2qSz_Y+~7=>WKhYhqZ~qC!;_9AvIMK@l#dkHfhQdg zh|{R<=y%Q>R+V2Am?Z&m&Nrwep zw>RW=ZD<5WKZ-)pFinSbgy9VApvSUw9p;-fFfSTU zn-L4$jxc4g3F(EYUA}4ifF&B*<}m0aw3D<6=raasIu5Y|;`$-uQG<{!XFl5a)NGFlPbv-&nAy zPq`#wB#bjUR{-n(u#xeGEi4JLUjGN9t)Sx#9XIHK2uVI!n!M#qpRGg~0#x97WGr2; zlkQx~Mk-f&9flM`XW8p?L#!}+A{Kn-=TTKftD%Mi-+SRMqV|}3;ze4u`5dHBi zELR%sled5+MWvb z;7v5 z@I76OEc0)2)=-m|K%fi+5sjfTQlrkaKribEC~A&Q!okF-czh%oGSSum4pko-0u89% zR*S>Y7L+H6R)YNG?l|EMjJb~)#je0E4mjCj^eUq*%5ck#{qt1l29)4x)r}(Nd#?0(Sgk6&b;T6$m|{Ov zq``Rt^H72N0@r*-uV6u`(U39{%a%pBCrr|G!#W>n@_1m;(7ItRSq;7|xkYHi_)egd zTic@UTgI>Sy2U(7X6sZs>7FwtBbi^ld%I|FIHk4C%|g*GIrleKAKY8pogI4tQ&1`W zurBi1BpM70RsFuH z!ky(x|ES&jc<}T6{*M>0DnrOh@A$a?{7vt}QUBAk%9xf)N@GSle=gh_zV%9+Qz+^u zfq10CKt~7-v@FUDAPqBsjJ=Dgci~#+|JDU<-}nl1Utq2?Z+4c}PryC}y90JNx6sKg Roa7cybBlXZsmGTT_+NOn*8czi literal 0 HcmV?d00001 diff --git a/task_center/asgi.py b/task_center/asgi.py new file mode 100644 index 0000000..30d87d2 --- /dev/null +++ b/task_center/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for task_center 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', 'task_center.settings') + +application = get_asgi_application() diff --git a/task_center/settings.py b/task_center/settings.py new file mode 100644 index 0000000..903e6bb --- /dev/null +++ b/task_center/settings.py @@ -0,0 +1,139 @@ +""" +Django settings for task_center 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 + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-&_@v3e5@!x+fa5273@v=^&02p@(#b89=p^06ifule17*p+g@u8' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'tasks', +] + +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 = 'task_center.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 = 'task_center.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +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 +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# REST Framework settings +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.AllowAny', + ], +} + +# Media files settings +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +# Test settings +# TEST_RUNNER = 'pytest_django.runner.DjangoPytestTestRunner' # Commented out as it's not needed for pytest diff --git a/task_center/urls.py b/task_center/urls.py new file mode 100644 index 0000000..99fbe54 --- /dev/null +++ b/task_center/urls.py @@ -0,0 +1,28 @@ +""" +URL configuration for task_center project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from tasks.views_frontend import index + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include('tasks.urls')), + # Root URL points to home page + path('', index, name='index'), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/task_center/wsgi.py b/task_center/wsgi.py new file mode 100644 index 0000000..af6dfcf --- /dev/null +++ b/task_center/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for task_center 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', 'task_center.settings') + +application = get_wsgi_application() diff --git a/tasks/__init__.py b/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/__pycache__/__init__.cpython-311.pyc b/tasks/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3bd103d95b6bf507c435d70e30f97134cd4bfa2 GIT binary patch literal 165 zcmZ3^%ge<81m}W{GC}lX5CH>>P{wCAAY(d13PUi1CZpdC%vKQO?EB4(f%0M~#nWdHyG literal 0 HcmV?d00001 diff --git a/tasks/__pycache__/admin.cpython-311.pyc b/tasks/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9941d0ce9c07e2a2517ddced49bcb856a39e535 GIT binary patch literal 220 zcmZ3^%ge<81m}W{GM#|*V-N=hn4pZ$LO{lJh7^Vr#vF!R#wbQch7_h?22JLdAO)I? zw^$QXax?S%G?{MkrDP@MrRVD<=jW9aWhNCd0~M@f_zY6_)d)y6RKx~U0RUu{KjZ)a literal 0 HcmV?d00001 diff --git a/tasks/__pycache__/apps.cpython-311.pyc b/tasks/__pycache__/apps.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e19ef92e7b317a0e722470a0b9252ea55ec69a70 GIT binary patch literal 534 zcmZ3^%ge<81m}W{GN%CP#~=<2utFK1^?;1&3@HpLj5!Rsj8Tk?3``8}3@J=43@Oa1 zjLVoA7*+!@1Vk~Vumm$`vc3dyH5qSlIu;Z-=jWwmrYA#HfGI{O=d&1)F`XfmA&N1D z0jQTTiaCWTiY0{^Xa>tI?vTXd>|%)aD%q5*#Ju!;y_6)q-29Z(oMJtv%yh@nl6<$! z)SQ%CtR*0|ewwVeIO5~;5_41I<8N`r#{-p?=788d@$rSFi8)Xip_J6L#L}FS_(Y%q z@o8WaSU_?s89sxY_SFzbcv!_eUAyeb&IK>Fwmh3QGX~;-m?yh;KkZuhWXIa4`98O4%7q5%$XNLpNA5xc=H)ZhYwMZ7Szewxg;xS^f{8B_!c zOo&lDAQo65ACUEn!v^9VyCMM~7ZirY-az65Gb1D84F<&vsOSS56Ql44225gt=0}j| J7c5d>%K(WrjSc_+ literal 0 HcmV?d00001 diff --git a/tasks/__pycache__/models.cpython-311.pyc b/tasks/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c263114d401ebd2e4ea30382ce84a4d668dc5080 GIT binary patch literal 4751 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 literal 0 HcmV?d00001 diff --git a/tasks/__pycache__/serializers.cpython-311.pyc b/tasks/__pycache__/serializers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cae9754a7b74e5decff7740cadc66042e8db981 GIT binary patch literal 3243 zcmb_e&5smC6tDiA@7ZPe{)$8p7(hf&AWBd(?qXt?gK*HYp;1B!N;qf3*6yPRO75(l18up_^0)xj`tQibHBjNl^$@sphCPt)$iTl3p`PhC)*RvcJ~HCPGBN&+hdEIAMK67iUltTeDP zYp~Lil?B$=8mtT*KS%QG9;2JQB2q5nRokh4&ba#+)`;XaVHTV!t9wE}Z@V)hiTDw7 zn~qn;PW=K)ElYPl+-?v?N(v<3|!cla#x?7Sh=DiwO&t}$VFjTu%K3CDIl%VqGGEOTai zj9Rvr*LW6u@iByPfMPx-;+BOyElVUVtJa`R2cOfHb-8IfK?_H12?H9LBg_j`=1^CR zgIQ`d>dsXwsJThHKqG&h1o+^s>E+u?4}Sml(Qnrte)aV{JbP8>I3u0{6X?@D1TeSyYmaZ5H`?n8zP#qm_V#!D;EHeY<;Ax^ z2fhuUcvI)=@Fj<^zF*BZ89Z*_@?4nTeF`y46Y@H0L!$61wp~e^j~qjLltoO^1>GUkH=m=21h}Ox!7=qS9#yiGjNZ8-_iw?mtFEpB<{NXHlre*swvUtX%%#mq)+;2w{wsFE*;cJwFMIl?EGqb*u%v++>^;4we|Nj7jKQ*vZembfk32?v^x?Gp&vNk=giCuOD_)l>zR zw9`(;%s85L+XqyZ!|W&z-|Q)y|i_*|yQmjkDNeNi8U^(4^%ee1oQa z=9{!FkL8H%6v1U5w+w{+vSm|kN)(U7^p?>IOvR>0lRVv{Sy6?EZXN~mis(}y{NXb8 z&@DT7UbONGylsK-hdZx}>_S|$K(ZX`jj^Ma>lZ~;3mq>IU4*x~wo2qUX0)+(5rd(p z^Sx93ONO;G0kq30j~D~Y_L%9?GRI^ilwzz z_0@OFU*FvM^5{t^rd?Wn|9^WT&clH3IUPh~)6`{M3fTJNCjo@gE_0`8O{3CnVmHYrebAI3e literal 0 HcmV?d00001 diff --git a/tasks/__pycache__/views.cpython-311.pyc b/tasks/__pycache__/views.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1bfa860b522d1d29f88f39ce17ba4307aade687 GIT binary patch literal 6821 zcmcIoYiJzT6~41OyBh6lq+MxOFIy|yvb>g~+Kv-azv?8iW5<#FkP@23@v_<(%Pa3g zxihj8u})0uABqtqlu$e1!X^YK#txK1OG+rEP}BS=%!&am11^TfrTMqU4hj9$bMEZS z>`HPVCA~X)=046nb6@8>=iE<%K_7v1W^!ltTal1|V!=wDdgh_P5ptDCMB;KJ&m}ny z^RApL?@qc|-ksy~o}?%5O?p|G&$Z-*By?94}#(R?f!%XcNa^4-bqd{456BQEj+k^Gm5 z6p%eLz1E;_xCyxhe={Zf7*`OuLf^x+j&X^=)%HDH>!tPyk_dl|q7$6aJ}s-M;?#_s zffr5f+P(`a1vQ(O=Zgh7;WC8tS^0t@tBT=O)U-OMK)W}cQM1JYyzRpGD5)bJfD(!81f8uo)E+=lo0To(Ep{A5}=XZVnU z1n8IW8f|9VgiN#PTy`D=gs{D#+~M#y)JCxO`CK}iUs*Y!!eAOIql~kdpUugt?9^w> zl{cxYUU>K-WUdmKBsqyBU9x+Ix7f+s{r3_yxl`l$jIm`!Asn_SyIkSZZL8Q@QZkl4a)47}>ygnz> zH({G-8#cBn_KKx4xwN8C0jtBaGR-b3)96@Uw!e#{VN}S_4#M_|NP5SDlDvWik*p(**9qnx-k-ljDkWg*}tmT zx%k$yIItuRRK$(Cxbf2AYHUd3JE}bit$VA+N6m*0SVEds-CEIy|A6KnXVqOq_Dh`P zn ZFjShN&IE9V!win-^2JrtN2@`mdo6V9%4g}wl#VPt9rCcUN{Lp!;j~%ehv;9 z{A^KCQ8y%6O=ojPOI}vb7A1wQhqcI9=MnP63x5QPhwFh}B_&dZ!s=AwW>DQ&y3O5y z{4KkD(N%Jl8)aII0FN+-CSHz|Tr>DRmL$I0%94xj5^?ll#@anHgCk^Vrp3 z@+?|A!=GV>is7@-GAkFPY+*X#qF(qUeF8{5Z$ugxQ>m;{n9JqpW?+~Pj~7>bQMt)6 z0po@MFrA$)fDMfrA+*X4hSROk*$}cy>O7c1$q2C*+S?S4mafBQ4_FI1N708cf~l!A zj6I(&q^H4Xx>M4W;a6$8pqQ2$EVx?CXEPKJy5T*O&8fiFuE;s`1I~aWFgFX5Wy5nO zOBI!(ma<_Q;lq=Y$I(qljUSs#9Xj^%_^9ExMxIiO2Ct;g%M_2A;b*)%w{K4!JDTu7 zi<~=Sv``srm7*HHhWX*@89cKs2pn-tbOTm;*jCNAH^AcvtLlSRu|T@rdf9o2;1im*c$c4)$mYBa7#fAZU)9^Jp- zhu)#^;%j;^t_Qd1!Kd}$ZasKtIXJcy9IFJ6>A_<)($U(*$o)v{{k_YP%}bHZ)mXgR zH&pYGj-dy{-4S|(=Q*B#Cz~V`6Kuz0PfQ_-gdz){@M?gogFDcuOs{oitg9S5a8b(k@wK}^W=+SgA)JwTTRHqrKglC`K{;&atDWw=67M*ClHfRspaQsuEHxfy=lC`&0Px#K7*wWVJeHI-RHlJTbw0gXW$d+Unw9M$ zuAWt*bv|Y}5%s9i)*AFyCh0Ix`T~;8K;~H}YAH}%W0?LhMM;F{VPwU~$vpW_Vh%CF zG`dGHw+jg_8hsIn5y;fPyh3vhKx4w=C=GoHyNn`v9*BZCf~r$Dt%4H13_j9(vL;B}$L&PXX;gf<6y(lPLNS%y}}A2ZJjXC{y%7 zDS|Ua50@T9(t%_Q3Hq4yIFdM$0VErcY_bT3#R|r)m+W6vj4YOx#r`F+zakFk;s8d& zgN@PfJ&*s=EAKs3e!9Fx+jm^^oTzwC=$;cb*E-KQS0hLtxQX{9=Tz<|-T?I3Ro)D~ zz}{n;=Xk|)T=yKWxqPgTL$T)x=xH@|HT68Hc_u2J3EeYMa|KyXhhoo3=($FdIdz$!)&fXt5*xyybzFzrfWL>@9Dc!*3vd7ZdU@=Bgn(X}Y?;PSm~F_Ar!Y&jRip06bB%>&c0hSIz}I3m&c<;VSYX zk5_~xzR`18+iQ@#&GC`ca$9I#Ay|pl`R>u;g+i{FmYAWBQ+Cr9hX6n};i?@X6f8Mm ziV0Q-lyJEhI0S9h1)t%fQ zpQ_lqP`Y+h?|X8&@0q2(XFl(!^zGOC_J1j9;zUK9(8UQ&m@u{E1bjZxwi1irwt5v7 zVSvppdK$0@Eg(gsW`t(hO{#T&XQI?=*c?aVfYAgNk-Q3jDp{M#GJ0RE-Xn=e5qAm&Q$8+EpGb$23o(;z{V9M9n35CIOTjiZHQt%@%V^IFj*1 zztL9TdgW}9s+l=eq3EF)fqM0vn$0Puj~Ie&hhUV#b?Aj6Jx9@eu`^_JH@A#Pa;8Wj zqNNI34x?v9yL0VXzX*-3V1OdKvSv2dh^}ZgS6rdaCU{w_t+1O|)1Kf)8h$6FRoF$W zX;;{WMsW1ZhM@T{0%I7h*AJ?BjfyzRHjKtGXYb^HguUiP@P3+w1`hskYd}cN&2e0n zjA+eIl?-UjPn8U7Ydux6O>2ItS&L`CM%H*%b2bXExmq|^jr@2d?B<4RWHov3 zu-=h?V453-VA^8w9pE^$p^tyPw?z*_AkP>cHS>+SL=MmT+Vq;BcRlfdH9mR_3){;7 E0<{9 literal 0 HcmV?d00001 diff --git a/tasks/__pycache__/views_frontend.cpython-311.pyc b/tasks/__pycache__/views_frontend.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c3b924fa28214fa028e69d248b8d48e860560b1 GIT binary patch literal 3359 zcmcguU2Gdw7QXXidz^8`b}3G_ODdWO{L*k94*`?{eL)n8L?3yau|~2rs1g!&p-&i_v=zMU zjpyXfz2}~L&$-|E&V1P0+e4te^wRygkq{w2;7hyltu|)MCEjn=cc$kKgH*T zDS^W=UKjI$sQ~l^Rn&v|&{QZdO-cFiR2aqrr^r-~8U!h;A&?PO0@Er%$ee`dFam+>CEVTqiN2S@|t1H&S}O~&RZuQ znKe$h!AJF+RxsTje0x?iO1havg?1C6BfxWQ2*ep;cFmQ+y;X2|t?f2}WAT$GV6|pP#z&&b!m<{A^*a zIDPrTg~przy?p+Y#y4+FGlSFr%xSM0GcVI(!32*pul(B$usx^(W`gh-cYs(Vm*n{A z-PYDUww!U~j3s8S!7%Z8h1$$36*}M*Dzw^s$HSG5U+p>kzY5PH1>di07PO-Pt_s=)yRMyNjj0_;$y9t{Po=9=by74c+{2;I`TnF zJV>Lk;^v__3xPO8%48liyryy|SV-tsFxm-|%KxRUKo+>}nVSa?QSXP%0{;hA1vab_ z{Gq>bqFd}6R73uKSHXw1jvou6iC55S^MQ761;MhRmANuc`7&SNR8kh8C$OG)R$7IW zS%b%G95FG(tv27;0iY80p9VA$zpbYB_)mA3n0L142;7P)`&T0)OuFJTPai(wiiO#{ z=E_-?*)!~gxIrUJbFY|g)Xe3zV#%B_v~01U8o9+^1p@Hv1M9RO#TFD>L6mSuFA*D+ z_Zk7mac=JC$fBiZ`+{r{2ptDq)H8n5YWjC1D%;QCsCcuCz03EqPu3$2V(@i)Q z^zEP9!)Hjm;dfWcT~iBV(@LfmKd%khF2EO9kUaAoy6XXT#xs^Jy45pug15p-1X5T zc6`K%k5qz}whS)My|34jnQAg)jXZpD*iIgDl85Xq6V8^2O0*$GmLfH2YgO7>kKb9} zeZSSee_39Z&x`fJ9o8LtPLH1+|7df4;GSlfga>{hfpE{SD4Hm)M@TsCNd3!w*3e#C z+UH37ENS1RXy3AIM|V2WotCt-mAwzx@~9(^TH>gew%z=MEW{YC0v$h3n_YTk?(90w zhUDYtI~pq;&H^)?wvO`zGu)X%D1wEupbGEv8(2@A1k&eost+ovX3pkx4=TJX;{9P^ zz|v!+0~GJqL&`4fVNQ6M5E&-i5WDc4N_XN6>KKTd^@#9xp}S#f8|pj)0&By-2S--s zEIH#)97vJI=Dfzo32CtlWd@C$-f%tIy}kf(IZ~GjjJgTX`T(hXF>6VRwyAMH$MH} zmC6@Cu6?$igz=_M_Io7Ux< z&2^`pcgzfqA}-dYT(56?dLL*oki7+++tBd$Qlg^ZUp>}Wi|wk$cC8$STxI z|JBL8)~$cdAjgd~Nk>RL_ZZhCT~dm2V@=W#Ljv~#D0ImP&ppYObfnbF{jEth3g+SO DhZD?~ literal 0 HcmV?d00001 diff --git a/tasks/admin.py b/tasks/admin.py new file mode 100644 index 0000000..ea5d68b --- /dev/null +++ b/tasks/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/tasks/apps.py b/tasks/apps.py new file mode 100644 index 0000000..ba66733 --- /dev/null +++ b/tasks/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TasksConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'tasks' diff --git a/tasks/management/__init__.py b/tasks/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/management/__pycache__/__init__.cpython-311.pyc b/tasks/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b01b4bce034cab3c48fdf9fe76854d77b6e86f90 GIT binary patch literal 176 zcmZ3^%ge<81ni+knIQTxh=2h`DC095kTIPhg&~+hlhJP_LlF~@{~09t)d5I&Sj9YD zyX?u%1uwR?JexK%CM7E|FFilz$?n}xyB0p#vG(cy=9rSi;_Tv>+{C=Z^wiwcypovs v_{_Y_lK6PNg34bUHo5sJr8%i~MXW&MK(-e11BnmJjEsyQ7+^#ZGf)fw-|sRc literal 0 HcmV?d00001 diff --git a/tasks/management/commands/__init__.py b/tasks/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/management/commands/__pycache__/__init__.cpython-311.pyc b/tasks/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebdeaf13e4ea845af17919036da481d7b7920765 GIT binary patch literal 185 zcmZ3^%ge<81cISPnIQTxh=2h`DC095kTIPhg&~+hlhJP_LlF~@{~09t)e}f~Sj9YD zyX?u%1uwR?JexK%CM7E|FFilz$?n}xyB0p#vG(cy=9rSi;_Tv>+{C=Z^wiwcypovY z{9GU}r8p)&J~J<~BtBlRpz;@oO>TZlX-=wL5i8JKkoCp4BO~Jn1{hJq3={(Z D8bCGW literal 0 HcmV?d00001 diff --git a/tasks/management/commands/check_task_timeouts.py b/tasks/management/commands/check_task_timeouts.py new file mode 100644 index 0000000..733b77b --- /dev/null +++ b/tasks/management/commands/check_task_timeouts.py @@ -0,0 +1,37 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from tasks.models import Task + +class Command(BaseCommand): + help = 'Check and update tasks that have timed out' + + def handle(self, *args, **kwargs): + now = timezone.now() + + # Get all tasks that are in progress (assigned, running, retrying) + in_progress_tasks = Task.objects.filter( + status__in=['assigned', 'running', 'retrying'] + ) + + timeout_count = 0 + + for task in in_progress_tasks: + # Calculate when the task should timeout + if task.started_at: + # If task has started, calculate from started_at + timeout_time = task.started_at + timezone.timedelta(seconds=task.timeout_seconds) + else: + # If task hasn't started, calculate from created_at + timeout_time = task.created_at + timezone.timedelta(seconds=task.timeout_seconds) + + # Check if task has timed out + if now > timeout_time: + task.status = 'timeout' + task.save() + timeout_count += 1 + self.stdout.write(self.style.WARNING(f'Task {task.id} "{task.name}" has timed out')) + + if timeout_count > 0: + self.stdout.write(self.style.SUCCESS(f'Successfully updated {timeout_count} timed out tasks')) + else: + self.stdout.write(self.style.INFO('No tasks have timed out')) diff --git a/tasks/migrations/0001_initial.py b/tasks/migrations/0001_initial.py new file mode 100644 index 0000000..64e5e3c --- /dev/null +++ b/tasks/migrations/0001_initial.py @@ -0,0 +1,65 @@ +# Generated by Django 5.0.6 on 2025-12-05 03:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Client', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='客户端标识')), + ('token', models.CharField(max_length=128, verbose_name='API Token')), + ('last_seen', models.DateTimeField(auto_now=True, verbose_name='最后活跃时间')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '客户端', + 'verbose_name_plural': '客户端', + }, + ), + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='任务名称')), + ('client_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='指定执行客户端')), + ('script', models.TextField(blank=True, null=True, verbose_name='执行脚本')), + ('status', models.CharField(choices=[('pending', '待分配'), ('assigned', '已分配'), ('running', '执行中'), ('success', '成功'), ('failed', '失败'), ('retrying', '重试中'), ('timeout', '超时,关闭')], default='pending', max_length=20, verbose_name='任务状态')), + ('timeout_seconds', models.IntegerField(default=259200, verbose_name='超时时间(秒)')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('assigned_to', models.CharField(blank=True, max_length=100, null=True, verbose_name='实际执行客户端')), + ('started_at', models.DateTimeField(blank=True, null=True, verbose_name='开始执行时间')), + ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')), + ], + options={ + 'verbose_name': '任务', + 'verbose_name_plural': '任务', + }, + ), + migrations.CreateModel( + name='TaskResult', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('result_file', models.FileField(blank=True, null=True, upload_to='task_results/', verbose_name='结果文件')), + ('status', models.CharField(choices=[('pending', '待分配'), ('assigned', '已分配'), ('running', '执行中'), ('success', '成功'), ('failed', '失败'), ('retrying', '重试中'), ('timeout', '超时,关闭')], max_length=20, verbose_name='执行状态')), + ('message', models.TextField(blank=True, null=True, verbose_name='执行消息')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.client', verbose_name='执行客户端')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='tasks.task', verbose_name='关联任务')), + ], + options={ + 'verbose_name': '任务结果', + 'verbose_name_plural': '任务结果', + }, + ), + ] diff --git a/tasks/migrations/__init__.py b/tasks/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/migrations/__pycache__/0001_initial.cpython-311.pyc b/tasks/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be66cc7e1547042c8c57f5f8b5f2402948b48c9b GIT binary patch literal 4101 zcmbtXU2qfE6<+;VvSb-cU~p^;OQvc-#x)MaxTR@Q$HqX44F!dQ#$;LRuEpA`m9i_* zm=rJ#1cyS0Nm_6cCMB5`rh@>tb-}HfmPa11X7)j|Pnjta_7h@oUpjs0xpyU5wmgNj zyV|q&=iKx2opaA#-Keg%GjO#9HVZ$mVVHlAPT{J^H=gxC;|hZqWDuF40p3Q@7%>G+ z5p&RNV2sQT2AR$<$jqCD3u~C`CWg5QkA4L!kYPXLub3m9Z_tPgK~tCsnvD!!F>EO;Kt^PO5$kYeam0WO$c!qImg}(o zCOrBzWJFfVX)BDNO5n65?ThUNzN#gB4pg07aoq^g;3-P0p`G?lo79$-2eX!0J+OU;011tw#;Zc+t4TR|7@Gom^XnUK8c4$^$_BH6TyY zyVzbpuX(Y=FoV{8WkBCTKJ@M6%Vkp6Q*K95>IURbwk*X5y%36MBb8Mek=A7*@}0%@ zffpnX+LrNnQ<*#iYG20VSC--1T+SD+LR;W|ew1JR22t*yx~q%$k*I7%oyqT(A-#ND zeCT`0ZDle1K9#yS)~}U&^n!8u0hP9*U?X2M>@2j0t;LqB*jia=q1VwH$sd+k*Y!Wk zdlPk|p5*qW*o3X97w%;u6E+1`p&d}ac51f1Lfr~zmTpntrMPAkP_Jg()#Ep74lWUo zvLTu0;ylu-VzLn7x>6h85nb~_EWUt(vyD|l}xRM$zMSrRpEA#B1N-CWBMn+ z9N?6ZxrZ=ERX&=T$(;W(bLrya8ofSM3R-4Kfsl7*P=i@s>~%xBrFNie1fq{$vasL2*34n8Y>D}Z8+@e&fGuo+fCf@Vv^#3+ZrZ0aRt*6qmyYLZ(B1Pgo2 z8iCkyVZMYldv`p0`i^GNQ}?)o=vUhG9u43U9AKB*q7nnzR;y&`reaym&a!X_ONczQ z9W489f)mMl=vf6xh(q1d9Ec}df|o)9ugK(Nr&V>+Lf1!*Z!$S!X^uCAa2J%oUb4S6 zTlY~;)|=HmU{nAyI6o8M-;3)_$1W+(hxt68try~@PuP2-G7mfLM^HR1Xqc12QO$@3 zwMvQ-oE$Z4ch~;zuAc3hJsRWlr^(+l&%hAopFKc!ZX3wly8h@d*Jl5GCVTnQ0bR&| z4g>6}%-=s4ppXtMoJI%Q+uL7Z^G>(LMrE=+DIp&z<=^1=Sn>XP#mccAIkTbGGPXNc z$=E#OcD1r8?W+IUwHnv8Tso%KwM?5I`f!lNl8oz=RGp&MDcBiLyXtVge`1YV@1NZN zpcV&sTtAemA5!awurr)?y@VTDCSFz>S|&pet8hag)euk{0@(R>+U3SgnO!N!rzfz3ta5s<(YI{?LT?9l-q0 zu=hmDdqVY|z|NBZ5^mf$u~lu{IO&@X;JxqQMmE*Rs*Nmm9x0j4F-(VOT0)PRPLZMA z=6*B04x2~F%PpFpQaVp5jJP7^v}+yqbzbXLeVq?1_-zsUA}L=)^+m8#`fqrZ%<0y7 z*p!D=J-T(zb*>nQY2SkYJ}6>OB;|>y9$l>mH+NiHqc(R;_dJN>;8ENxq?!e_S-{TW zT%7{~GhjR0_9mn0tIBAklcoaK&^d&K$RH$z=!v7ICc_yPo=!4RPQMQRG0C2 zkdK<)Cy>8oK1i`XM6o_(CRiULBd#D|-GJS#6TPatb@J6|gn!Ip_mPzQi0VFqom>f` zW!XuUw~}mVojf)*s&?*BJNK%c2l3$%5dFSkw-H|MJ|l^5pONV7BO|VUFdH^f3)x65 zlLw{_sU6$Zj()WR(oMn&acz(BFkx+Yhb#sPi-D5GtT9(p30T)M&70EeJ4&#wt07ne znI|N-!?0zgW$Z0-%l(CN;J0Y?dO>MJgKhd>*tSAxl6L}|W-HFqdPz&9UQhan6e?;D zwEk83nqGZhm-j)}0FN>VBxf=h3^@y9XvU0J|D~A?c)4HBvdv)0G3DfvU3L1Nl8O0c IW~mMT0Wgx%>P{wCAAY(d13PUi1CZpd + + + + + 任务中心 + + + + + +
+ {% block content %} + {% endblock %} +
+ + + + \ No newline at end of file diff --git a/tasks/templates/tasks/client_create.html b/tasks/templates/tasks/client_create.html new file mode 100644 index 0000000..7637f40 --- /dev/null +++ b/tasks/templates/tasks/client_create.html @@ -0,0 +1,25 @@ +{% extends 'tasks/base.html' %} + +{% block content %} +

创建客户端

+ +
+
+
+ {% csrf_token %} + +
+ + +
+ + + + + 取消 +
+
+
+{% endblock %} \ No newline at end of file diff --git a/tasks/templates/tasks/client_list.html b/tasks/templates/tasks/client_list.html new file mode 100644 index 0000000..57c49f8 --- /dev/null +++ b/tasks/templates/tasks/client_list.html @@ -0,0 +1,37 @@ +{% extends 'tasks/base.html' %} + +{% block content %} +

客户端列表

+ +
+ +
+ + + + + + + + + + + + {% for client in clients %} + + + + + + + + {% endfor %} + +
ID客户端名称API Token最后活跃时间创建时间
{{ client.id }}{{ client.name }} + {{ client.token }} + {{ client.last_seen|date:'Y-m-d H:i:s' }}{{ client.created_at|date:'Y-m-d H:i:s' }}
+
+
+{% endblock %} \ No newline at end of file diff --git a/tasks/templates/tasks/index.html b/tasks/templates/tasks/index.html new file mode 100644 index 0000000..dba7006 --- /dev/null +++ b/tasks/templates/tasks/index.html @@ -0,0 +1,96 @@ + + + + + + 任务中心 - 首页 + + + + + +
+
+

欢迎使用任务中心

+

一个基于Django的任务管理系统,支持客户端任务认领、执行和结果上传。

+
+

系统功能:

+
    +
  • 创建和管理任务
  • +
  • 客户端原子认领任务
  • +
  • 任务执行状态跟踪
  • +
  • 结果文件上传下载
  • +
  • 客户端管理和认证
  • +
+ +
+ +
+
+
+
+
任务管理
+

创建、查看和管理任务,支持设置执行客户端和超时时间。

+ 进入 +
+
+
+
+
+
+
客户端管理
+

管理客户端列表,自动生成API Token,用于客户端认证。

+ 进入 +
+
+
+
+
+
+
后台管理
+

使用Django Admin进行系统管理,包括用户、任务和客户端。

+ 进入 +
+
+
+
+
+ +
+
+

© 2025 任务中心管理系统

+
+
+ + + + \ No newline at end of file diff --git a/tasks/templates/tasks/task_create.html b/tasks/templates/tasks/task_create.html new file mode 100644 index 0000000..b5a41fd --- /dev/null +++ b/tasks/templates/tasks/task_create.html @@ -0,0 +1,37 @@ +{% extends 'tasks/base.html' %} + +{% block content %} +

创建任务

+ +
+
+
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
默认3天(259200秒)
+
+ + + 取消 +
+
+
+{% endblock %} \ No newline at end of file diff --git a/tasks/templates/tasks/task_detail.html b/tasks/templates/tasks/task_detail.html new file mode 100644 index 0000000..3ccd24a --- /dev/null +++ b/tasks/templates/tasks/task_detail.html @@ -0,0 +1,98 @@ +{% extends 'tasks/base.html' %} + +{% block content %} +

任务详情

+ +
+
+
{{ task.name }}
+
+
+
+
+

ID: {{ task.id }}

+

指定客户端: {{ task.client_name|default:'未指定' }}

+

实际执行客户端: {{ task.assigned_to|default:'未分配' }}

+

状态: + + {{ task.get_status_display }} + +

+
+
+

创建时间: {{ task.created_at|date:'Y-m-d H:i:s' }}

+

更新时间: {{ task.updated_at|date:'Y-m-d H:i:s' }}

+

开始执行时间: {{ task.started_at|default:'未开始'|date:'Y-m-d H:i:s' }}

+

完成时间: {{ task.completed_at|default:'未完成'|date:'Y-m-d H:i:s' }}

+

超时时间: {{ task.timeout_seconds }} 秒

+
+
+ + {% if task.script %} +
+
执行脚本:
+
{{ task.script }}
+
+ {% endif %} +
+
+ +

执行结果

+ +
+
+ {% if results %} + + + + + + + + + + + + + {% for result in results %} + + + + + + + + + {% endfor %} + +
ID执行客户端状态执行消息创建时间结果文件
{{ result.id }}{{ result.client.name }} + + {{ result.get_status_display }} + + {{ result.message|default:'无消息' }}{{ result.created_at|date:'Y-m-d H:i:s' }} + {% if result.result_file %} + 下载 + {% else %} + 无文件 + {% endif %} +
+ {% else %} +

暂无执行结果

+ {% endif %} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/tasks/templates/tasks/task_list.html b/tasks/templates/tasks/task_list.html new file mode 100644 index 0000000..e6043c2 --- /dev/null +++ b/tasks/templates/tasks/task_list.html @@ -0,0 +1,48 @@ +{% extends 'tasks/base.html' %} + +{% block content %} +

任务列表

+ +
+ +
+ + + + + + + + + + + + + {% for task in tasks %} + + + + + + + + + {% endfor %} + +
ID任务名称指定客户端状态创建时间操作
{{ task.id }}{{ task.name }}{{ task.client_name|default:'未指定' }} + + {{ task.get_status_display }} + + {{ task.created_at|date:'Y-m-d H:i' }} + 详情 +
+
+
+{% endblock %} \ No newline at end of file diff --git a/tasks/tests.py b/tasks/tests.py new file mode 100644 index 0000000..de8bdc0 --- /dev/null +++ b/tasks/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/tasks/tests/__init__.py b/tasks/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tasks/tests/__pycache__/__init__.cpython-311.pyc b/tasks/tests/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c86400a23941513ac927557eab26bad1d42cce8 GIT binary patch literal 171 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 literal 0 HcmV?d00001 diff --git a/tasks/tests/__pycache__/test_api.cpython-311.pyc b/tasks/tests/__pycache__/test_api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d130bc791a872a7c02920da616cf59965e6a67ae GIT binary patch literal 8227 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 j0T#mnZ+dC^OSgKzRN;rX literal 0 HcmV?d00001 diff --git a/tasks/tests/__pycache__/test_factories.cpython-311-pytest-8.3.2.pyc b/tasks/tests/__pycache__/test_factories.cpython-311-pytest-8.3.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b63e90add074050382421807bf13d8420b510c33 GIT binary patch literal 3345 zcmbtW&2Jk;6rc6Cy}OR%rfHg_X`2XDu7stwp@8%3}Q%SQjo+qWQL0&iI|Z>RPs2n zLJT;ea@4(&= zmYrf#JMjUJiL$*0pL>J~?-W8p4+-=Lt%blMs9q_ALxe_Ve7_PSyg=3@kbv5hhzW_f z-!~!weggQ(8S&3h2KW8m7bXSyU%84A1$$#$^=!MO7o7^5Z!^eOWIGFswhW@-LPR(# z^fcQxIe(Qp`gktN6Pjk}Ce<{a(loPTRLj^_H0@?pFMAw@-r`YU((-+>javWx7La$( zO+IJ{fjRrjTzmVu1r3D^ytaw-#-57!INJ;n{>ec_M}5?$D`T}p?rzj z`MG%>K`j_MB?i?#0L0yW8kye3C_XF$;|vWe`es5Kf|3x7xorgqM7i`S`HFlKnjrT> z<2+szVW6=tVBVLwWZ5|P80!Z5H1T>_H?JG|h1QrW_#ZYe^wko%H zvaH*VW>aeMRFP5Lp@ybALT%4(RG1}vf+1+n18MnbGbBY1ZIVv1g=0G{MvN$fi?7z@ zfsMg4@Ydwfx;*M$Y$%5p$L?HRIZ;!F>&meEP9rn8lvq~orkB(1dyVcROMT0OcZZgT z+{^fQTA@O10T_V+EZe^VX$9c_MpO%Pkq*qo1?Er{QH*E+b4epsNEvZ~E~S;X0@zE# zDtmLgj6@*=wHs>INB|C#T*j>OVkpc!Gu6jnFp$t>hPgEu1qcE55)cgP4z%>V7+P|G zmMFs1!wT!gwOd$WebB$%^Q_uFNKhRgB(v-K>g?*Q>w#o{IAuMsfM?_WH^m&$+;n~F z;m!4_^{M8jJmSz#93G#emQk{%OBrzCc>NsQmmMB zW%_)GkKkHJ@Xy=pK-wXG5Fs8U2=Rjm@h0OLh@aXRJXMoV*X7dy@$B%@iF$T;W8@sX zwe0zN_PqOkL+xL@c4unkY)u`mtHbW4hT03Hx8MDs4fXAvY%k30KImR<9601&!Nte^tk(=!MmQ3BZ@V}!+7 z+l>+L-5g#F>Af8b?1gtc`}W5BS>O&q?ZA6~5btp6+0|l^+BVCl)a2P3S`7h*g&7A0R#kd)hJFtf|Kk8AnoBifZ-e@ z7|sJ2&P@h{^AMg`M^$arI8J68}0~?|W?v LSv$|+3y1sz=yK%= literal 0 HcmV?d00001 diff --git a/tasks/tests/__pycache__/test_factories.cpython-311.pyc b/tasks/tests/__pycache__/test_factories.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9491d2480441bb011fa5d8858d1ee8c1eeaac2d GIT binary patch literal 3210 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 literal 0 HcmV?d00001 diff --git a/tasks/tests/__pycache__/test_models.cpython-311-pytest-8.3.2.pyc b/tasks/tests/__pycache__/test_models.cpython-311-pytest-8.3.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b886f1368a9bdf0b0bbc829f770fe9fddde85a79 GIT binary patch literal 6962 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 V8k6v=wU(<#p7N4sZOZvq{SVaqo!I~Y literal 0 HcmV?d00001 diff --git a/tasks/tests/__pycache__/test_views.cpython-311-pytest-8.3.2.pyc b/tasks/tests/__pycache__/test_views.cpython-311-pytest-8.3.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77c7b36b4e2bbb2cd68f97dbd42e7c7aadda5323 GIT binary patch literal 4529 zcmeHKTWl0n7(TPtvzHFg7ATaYlR}G&p%z*MZ$MqDJoUw5%%;g^*%``~+1+|(3Te|O zsC}r0hsGF-@ntopmK5~Kc;UfUA7^nAn|U&kMR06a5j>NWTS~FZ~OI@telXu+Y;L(;vugSH84-qpcY*A60!=fD2eS{ zMnM*;lkHHYH%Kh}098j~l0!!|!;Gg44eCLrUDg;>Jx(C5XSAH@_(oI4C1|&Z_G_uM znP<~Z1lb#n^v1OOPugg#D^OV?8sBolqe}B6;Z;3AeX19zU-iK<{7w`HdNZS4!DXb& zy>Sp9ytt)x8MsTttdLE(byZr1`l{QOh*>^(=&E|ErE}6OnQ)J(fHTyU*_sjOJhP-w zG3I86SwGl7w&&EfA8Z@#?*;aZahCG7JK)*Tcsc08{=CVGvRa$PLp^b zM&IPL(}v8XyIC%nQAR89*VjM;FVT1h$W`(rKtk;{RSG+bbiYmaTXcU(-nn?GD0ka( z_kwq=ZTCknEuH%CRAHdlcF=A+xIllU(O+oC9olid!`^dv#VFDNn+{lXU=0ObcW76E zT8B;->5xr_EIP#5op)&G^|OUsksh<@F{}Kn!y-sy6S)VHY7?n73=VzQObRYgam;dQ z2z}RF%3Amj(07dojI18|O$?@?nYcz&Jwv^jsD6NcV5YwT^aV1TfZvJmOBXs!hG}MY zahM3?3;rhdOADWtZvP-aHF6`!fzGx=FSY|oE0Q)O?MQYa*#)GGRJIoy&dyCJZCDNB zaHhKn+$wZ%Xd?&YDnWE*i}C?mKJXOidbLR7HjP^}z7@LIKA5sGy7mEXs?oI!x~uSt zGAEf8C{d+ZX~MCI`I53nw zO#rEwOxWasI8V6BI1m|2tyT|@ghB8(-T(t z*&?j`r;?VV)k%xEF-3&^W+9?w<6ec90AYtnT&g4$FyyI=3&s)Fh4`z_PImL`q-qcW zuikibcva*m#}Ht74lqPhz9{$Fa_>{P_%lU1Y|~+j4p-zTP0%zF^Rs7gUkGz{2uVMZ z0VK~N!Kfr$`C+Uf!4AezAl0t?1$5RM1Y9}P>WLT*2Z^;h^!!vtH{pB5iJqREPP(l< z$;oLv)IRZ5lga1$n0AF_Ow9?oEd0P0yfAd;caUOEM100@Q)ZY*xg7WsGDNtK@@wyd zSf&9BvulRI$=NITIFuoL!3F+>p3w}(9T288BB1f3s6L)Q=*uEHo@6IM52KOsGmwW~ zNs`to&ua73ux1gZ=2;_MR`s(+daOpz8X2-0Jr8|~1W~h|ES`9T+{c@uGg7+*H&ss- O-+F}H#|=dM68{8HN-4Pj literal 0 HcmV?d00001 diff --git a/tasks/tests/__pycache__/test_views.cpython-311.pyc b/tasks/tests/__pycache__/test_views.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31b2db92a35ffe292e4b79ebc82dbe137da8c91d GIT binary patch literal 4393 zcmeHKO>7iZ9DlPjyZidJ0xeJ|NhgIC7eg(yNd3gRR5|U%V$7z=X5AUemi@r@rWl$w zLG7U$4#XIX@v<0GOA2~2q8z+>oF$Xk%*jL+;Z{<(c=G>$Z+E(%H4#1O+nL|I_x}HR zU-SEa?AIL~tpv*D(??T}C?UULqg?!@%Ekq#ED?hkN}6Ppn4)0am-c1Um@4XOTFdxj z{!Abi5N$0T%+MIkw8UBz;v;Vm!@od`fT=92gsj3XORQDMf*_-HvNogTO%iT>fU04| zr4uH%A_;CnJ;=-pCWETa4Mfr@Gi$lpM1r4#c86@go=93bcF}Fa?ouOJnpHfMQe#7h z$`Ub2Ofg8zXDACKrW!s#%}@dTh6bzfyX`p8TPgEBTt>3|HV&e}jXT;1LGKc=DrD1M z-B6aHzUs9VVwF!9UNwBx(s^Z$Onax4#~JF%T+N8{zB!Vw7<2Q5Ip1`k+%nWb8t4K?>I>-lYjuqPoLg{q5YJ1u&L$8{-GWop(<$h>0B5N)Gi$(y z)RQ^J%%qjR_yBk9wxC)Z)gJr|?b$AD^Jl%@!LwE-J<|@NySHx_KfYF6T)Ka4B^qvF zxIDK#$;^ah#@|U;$+HYk&kZCcP1GsVa@!J|o6H)2cP5c`+qjjmW_UcAGfa1fluVeJ z^Jy62G&c=5B>N({td&S*`E}LRxS2lVsxvI@QfBh=xhyw1E?n33$1lV4)C-fvo6C29 z$lw2N;ofJTPa4yS>{M=2F81!No5i0#oD^G{wBU5Oz&IWVA3T52?ZnQww^<>W)h8$L)nI5c9@$X4uP`(Vn(?Aiyssb<$Q=&r&o%e-P$u*6X2lxdG&Q_77R zexO7BODA0GxYedkSp8d0SgmIl%+S~gtFWJz@?LAXz6B=@6C7K5YfefX2)$Z@P4Rke zO1a=>G~6oOuynk>l(97@T`il)m|>0WhNZh2@||@+m(?U$5+C3XrvGtaY7^iIDw6?QUyBX&>_2g2?|HU0hh+( zxcPY8ZH>n>Ib$Y`_4ariBa@V{9#}Nf5e@=eC*oys91Jrz0qB|d7{Dd(x9)58KetXi z=ni7jgB`8@BPgu}IXVuUgxf1+hrEG4qgl2DPET_~GXMHNQ)*(Q%`>H%a3K?xt#T!X z36R?+6-BmGWtSEGGG;sl>cwxqytnxM@2lT#rc9VPFqAz>0I8Tv+T?+VNVv*8kQqy@ zRv2dG9{|XlWtT(y@<$4^-=Y0B?f(~}ial_=Kt~-qYL~Aq!peUtX*pV*w8$@}%dp=p zM0AwCtJIPt?3Rg3m861(B6ab|7-hZ4zxwQCx5!Sa29fj{h%~2HMUHX|36>KCLpJ3K zp#di}@B}XYWPy%5blj%n6*)>1HbujJ_B8GbY0eHI3?U37JcEEyNxJgGSVO=U_)&mr zSN=RY>k@z~hgv>xZQ1&e>#NWF-&{B9{AtFWpHj6A88OGj|p9v3o{HQ?q1oC zhtH+uo)Q12QzmD^U0?zt!;e^a^=AH{FUeT;3L6DIj2-+Iz`Cj^${Nw_>T8Yk+SS(@ u>9-qQYh=uBbggT;0)ea^u8cm!-bb6FlS-EYhgT0*-hPO^j~a-?x&8@cs`|kI literal 0 HcmV?d00001 diff --git a/tasks/tests/test_api.py b/tasks/tests/test_api.py new file mode 100644 index 0000000..ff5f7fe --- /dev/null +++ b/tasks/tests/test_api.py @@ -0,0 +1,128 @@ +from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient +from tasks.models import Client, Task, TaskResult +from tasks.tests.test_factories import ClientFactory, TaskFactory, TaskResultFactory + +class TaskAPITest(TestCase): + def setUp(self): + self.client = APIClient() + # Create a test client with token + self.test_client = ClientFactory() + self.token = self.test_client.token + # Authenticate the client + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token}') + + def test_task_list(self): + """Test that authenticated users can list tasks""" + # Create some test tasks + TaskFactory.create_batch(3) + + url = reverse('task-list') + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 3) + + def test_task_create(self): + """Test that authenticated users can create tasks""" + url = reverse('task-list') + data = { + 'name': 'test_task', + 'client_name': 'test_client', + 'script': 'echo "Hello World"', + 'timeout_seconds': 3600 + } + + response = self.client.post(url, data, format='json') + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Task.objects.count(), 1) + self.assertEqual(Task.objects.get().name, 'test_task') + + def test_task_claim(self): + """Test that clients can claim available tasks""" + # Create a pending task + TaskFactory(client_name='test_client') + + url = reverse('task-claim') + data = { + 'client_name': 'test_client' + } + + response = self.client.post(url, data, format='json') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['status'], 'assigned') + self.assertEqual(response.data['assigned_to'], 'test_client') + + def test_unauthenticated_access(self): + """Test that unauthenticated users cannot access API endpoints""" + # Create a new client without authentication + unauth_client = APIClient() + + url = reverse('task-list') + response = unauth_client.get(url) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + +class ClientAPITest(TestCase): + def setUp(self): + self.client = APIClient() + # Create a test client with token + self.test_client = ClientFactory() + self.token = self.test_client.token + # Authenticate the client + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token}') + + def test_client_list(self): + """Test that authenticated users can list clients""" + # Create some test clients + ClientFactory.create_batch(3) + + url = reverse('client-list') + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 4) # Including the test client + + def test_client_create(self): + """Test that authenticated users can create clients""" + url = reverse('client-list') + data = { + 'name': 'new_client' + } + + response = self.client.post(url, data, format='json') + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Client.objects.count(), 2) # Including the test client + self.assertEqual(Client.objects.get(id=response.data['id']).name, 'new_client') + +class TaskResultAPITest(TestCase): + def setUp(self): + self.client = APIClient() + # Create a test client with token + self.test_client = ClientFactory() + self.token = self.test_client.token + # Authenticate the client + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token}') + # Create a test task + self.task = TaskFactory() + + def test_task_result_create(self): + """Test that authenticated users can create task results""" + url = reverse('taskresult-list') + data = { + 'task': self.task.id, + 'client': self.test_client.id, + 'status': 'success', + 'message': 'Task completed successfully' + } + + response = self.client.post(url, data, format='json') + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(TaskResult.objects.count(), 1) + self.assertEqual(TaskResult.objects.get().status, 'success') diff --git a/tasks/tests/test_factories.py b/tasks/tests/test_factories.py new file mode 100644 index 0000000..d8d7e06 --- /dev/null +++ b/tasks/tests/test_factories.py @@ -0,0 +1,38 @@ +import factory +from django.utils import timezone +from tasks.models import Client, Task, TaskResult + +class ClientFactory(factory.django.DjangoModelFactory): + class Meta: + model = Client + + name = factory.Sequence(lambda n: f"client_{n}") + token = factory.Faker('uuid4') + last_seen = timezone.now() + created_at = timezone.now() + +class TaskFactory(factory.django.DjangoModelFactory): + class Meta: + model = Task + + name = factory.Sequence(lambda n: f"task_{n}") + client_name = factory.Sequence(lambda n: f"client_{n}") + script = factory.Faker('text') + status = 'pending' + timeout_seconds = 3600 # 1 hour + created_at = timezone.now() + updated_at = timezone.now() + assigned_to = None + started_at = None + completed_at = None + +class TaskResultFactory(factory.django.DjangoModelFactory): + class Meta: + model = TaskResult + + task = factory.SubFactory(TaskFactory) + client = factory.SubFactory(ClientFactory) + result_file = None + status = 'success' + message = factory.Faker('text') + created_at = timezone.now() diff --git a/tasks/tests/test_integration.py b/tasks/tests/test_integration.py new file mode 100644 index 0000000..1102dc5 --- /dev/null +++ b/tasks/tests/test_integration.py @@ -0,0 +1,89 @@ +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone +from rest_framework import status +from rest_framework.test import APIClient +from tasks.models import Client, Task, TaskResult +from tasks.tests.test_factories import ClientFactory, TaskFactory + +class TaskFlowIntegrationTest(TestCase): + def setUp(self): + self.client = APIClient() + # Create a test client with token + self.test_client = ClientFactory() + self.token = self.test_client.token + # Authenticate the client + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token}') + + def test_full_task_flow(self): + """Test the full task flow: create → claim → start → complete""" + # 1. Create a task + create_url = reverse('task-list') + create_data = { + 'name': 'integration_test_task', + 'client_name': self.test_client.name, + 'script': 'echo "Integration Test"', + 'timeout_seconds': 3600 + } + create_response = self.client.post(create_url, create_data, format='json') + self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + task_id = create_response.data['id'] + + # 2. Claim the task + claim_url = reverse('task-claim') + claim_data = { + 'client_name': self.test_client.name + } + claim_response = self.client.post(claim_url, claim_data, format='json') + self.assertEqual(claim_response.status_code, status.HTTP_200_OK) + self.assertEqual(claim_response.data['status'], 'assigned') + self.assertEqual(claim_response.data['assigned_to'], self.test_client.name) + + # 3. Start the task + start_url = reverse('task-start', args=[task_id]) + start_response = self.client.post(start_url, format='json') + self.assertEqual(start_response.status_code, status.HTTP_200_OK) + self.assertEqual(start_response.data['status'], 'running') + self.assertIsNotNone(start_response.data['started_at']) + + # 4. Complete the task + complete_url = reverse('task-complete', args=[task_id]) + complete_data = { + 'status': 'success', + 'message': 'Task completed successfully' + } + complete_response = self.client.post(complete_url, complete_data, format='json') + self.assertEqual(complete_response.status_code, status.HTTP_200_OK) + self.assertEqual(complete_response.data['status'], 'success') + self.assertIsNotNone(complete_response.data['completed_at']) + + # 5. Verify the task status in database + task = Task.objects.get(id=task_id) + self.assertEqual(task.status, 'success') + self.assertEqual(task.assigned_to, self.test_client.name) + self.assertIsNotNone(task.started_at) + self.assertIsNotNone(task.completed_at) + + def test_task_result_upload(self): + """Test that a client can upload task results""" + # Create a test task + task = TaskFactory() + + # Upload a task result + upload_url = reverse('taskresult-list') + upload_data = { + 'task': task.id, + 'client': self.test_client.id, + 'status': 'success', + 'message': 'Result uploaded successfully' + } + + upload_response = self.client.post(upload_url, upload_data, format='json') + self.assertEqual(upload_response.status_code, status.HTTP_201_CREATED) + + # Verify the result exists in database + self.assertEqual(TaskResult.objects.count(), 1) + result = TaskResult.objects.get() + self.assertEqual(result.task, task) + self.assertEqual(result.client, self.test_client) + self.assertEqual(result.status, 'success') diff --git a/tasks/tests/test_models.py b/tasks/tests/test_models.py new file mode 100644 index 0000000..542a6b0 --- /dev/null +++ b/tasks/tests/test_models.py @@ -0,0 +1,109 @@ +from django.test import TestCase +from django.utils import timezone +from tasks.models import Client, Task, TaskResult + +class ClientModelTest(TestCase): + def test_client_creation(self): + """Test that a client can be created with all required fields""" + client = Client.objects.create( + name="test_client", + token="test_token_12345" + ) + + self.assertEqual(client.name, "test_client") + self.assertEqual(client.token, "test_token_12345") + self.assertIsNotNone(client.created_at) + self.assertIsNotNone(client.last_seen) + + def test_client_str(self): + """Test that the client string representation is correct""" + client = Client.objects.create( + name="test_client", + token="test_token_12345" + ) + + self.assertEqual(str(client), "test_client") + +class TaskModelTest(TestCase): + def test_task_creation(self): + """Test that a task can be created with all required fields""" + task = Task.objects.create( + name="test_task", + client_name="test_client", + script="echo 'Hello World'", + timeout_seconds=3600 + ) + + self.assertEqual(task.name, "test_task") + self.assertEqual(task.client_name, "test_client") + self.assertEqual(task.script, "echo 'Hello World'") + self.assertEqual(task.status, "pending") + self.assertEqual(task.timeout_seconds, 3600) + self.assertIsNotNone(task.created_at) + self.assertIsNotNone(task.updated_at) + + def test_task_str(self): + """Test that the task string representation is correct""" + task = Task.objects.create( + name="test_task" + ) + + self.assertEqual(str(task), "test_task") + + def test_task_status_choices(self): + """Test that task status choices are correctly implemented""" + from tasks.models import STATUS_CHOICES + status_choices = [choice[0] for choice in STATUS_CHOICES] + + self.assertIn("pending", status_choices) + self.assertIn("assigned", status_choices) + self.assertIn("running", status_choices) + self.assertIn("success", status_choices) + self.assertIn("failed", status_choices) + self.assertIn("retrying", status_choices) + self.assertIn("timeout", status_choices) + +class TaskResultModelTest(TestCase): + def test_task_result_creation(self): + """Test that a task result can be created with all required fields""" + client = Client.objects.create( + name="test_client", + token="test_token_12345" + ) + + task = Task.objects.create( + name="test_task" + ) + + result = TaskResult.objects.create( + task=task, + client=client, + status="success", + message="Task completed successfully" + ) + + self.assertEqual(result.task, task) + self.assertEqual(result.client, client) + self.assertEqual(result.status, "success") + self.assertEqual(result.message, "Task completed successfully") + self.assertIsNotNone(result.created_at) + + def test_task_result_str(self): + """Test that the task result string representation is correct""" + client = Client.objects.create( + name="test_client", + token="test_token_12345" + ) + + task = Task.objects.create( + name="test_task" + ) + + result = TaskResult.objects.create( + task=task, + client=client, + status="success" + ) + + # Expected status display is in Chinese + self.assertEqual(str(result), f"{task.name} - {client.name} - 成功") diff --git a/tasks/tests/test_views.py b/tasks/tests/test_views.py new file mode 100644 index 0000000..14396a7 --- /dev/null +++ b/tasks/tests/test_views.py @@ -0,0 +1,60 @@ +from django.test import TestCase +from django.urls import reverse +from tasks.models import Client, Task +from tasks.tests.test_factories import ClientFactory, TaskFactory + +class TaskViewTest(TestCase): + def test_task_list_view(self): + """Test that the task list view renders correctly""" + # Create some test tasks + TaskFactory.create_batch(3) + + url = reverse('task_list') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'tasks/task_list.html') + self.assertContains(response, '任务列表') + + def test_task_create_view(self): + """Test that the task create view renders correctly""" + url = reverse('task_create') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'tasks/task_create.html') + self.assertContains(response, '创建任务') + + def test_task_detail_view(self): + """Test that the task detail view renders correctly""" + # Create a test task + task = TaskFactory() + + url = reverse('task_detail', args=[task.id]) + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'tasks/task_detail.html') + self.assertContains(response, task.name) + +class ClientViewTest(TestCase): + def test_client_list_view(self): + """Test that the client list view renders correctly""" + # Create some test clients + ClientFactory.create_batch(3) + + url = reverse('client_list') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'tasks/client_list.html') + self.assertContains(response, '客户端列表') + + def test_client_create_view(self): + """Test that the client create view renders correctly""" + url = reverse('client_create') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'tasks/client_create.html') + self.assertContains(response, '创建客户端') diff --git a/tasks/urls.py b/tasks/urls.py new file mode 100644 index 0000000..953644d --- /dev/null +++ b/tasks/urls.py @@ -0,0 +1,23 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import ClientViewSet, TaskViewSet, TaskResultViewSet +from .views_frontend import ( + index, task_list, task_create, task_detail, + client_list, client_create +) + +router = DefaultRouter() +router.register(r'clients', ClientViewSet) +router.register(r'tasks', TaskViewSet) +router.register(r'task_results', TaskResultViewSet) + +urlpatterns = [ + path('', include(router.urls)), + # Frontend views + path('index/', index, name='index'), + path('tasks/list/', task_list, name='task_list'), + path('tasks/create/', task_create, name='task_create'), + path('tasks//', task_detail, name='task_detail'), + path('clients/list/', client_list, name='client_list'), + path('clients/create/', client_create, name='client_create'), +] diff --git a/tasks/views.py b/tasks/views.py new file mode 100644 index 0000000..4ede97b --- /dev/null +++ b/tasks/views.py @@ -0,0 +1,105 @@ +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.parsers import MultiPartParser, FormParser +from .models import Client, Task, TaskResult +from .serializers import ( + ClientSerializer, + TaskSerializer, + TaskResultSerializer, + TaskClaimSerializer, + TaskStartSerializer, + TaskCompleteSerializer +) + +class ClientViewSet(viewsets.ModelViewSet): + queryset = Client.objects.all() + serializer_class = ClientSerializer + permission_classes = [IsAuthenticated] + +class TaskViewSet(viewsets.ModelViewSet): + queryset = Task.objects.all() + serializer_class = TaskSerializer + permission_classes = [IsAuthenticated] + + @action(detail=False, methods=['post'], serializer_class=TaskClaimSerializer) + def claim(self, request): + """Client claims an available task""" + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + client_name = serializer.validated_data['client_name'] + + # Try to find a pending task, either assigned to this client or unassigned + with self.queryset.model.objects._base_manager._db.transaction.atomic(): + # First try to find tasks assigned to this client + task = Task.objects.filter( + status='pending', + client_name=client_name + ).select_for_update().first() + + # If no task assigned to this client, try to find unassigned tasks + if not task: + task = Task.objects.filter( + status='pending', + client_name__isnull=True + ).select_for_update().first() + + if not task: + return Response({'detail': 'No available tasks'}, status=status.HTTP_404_NOT_FOUND) + + # Assign the task to the client + task.status = 'assigned' + task.assigned_to = client_name + task.save() + + return Response(TaskSerializer(task).data, status=status.HTTP_200_OK) + + @action(detail=True, methods=['post'], serializer_class=TaskStartSerializer) + def start(self, request, pk=None): + """Client starts a task""" + task = self.get_object() + if task.status != 'assigned': + return Response({'detail': 'Task is not assigned'}, status=status.HTTP_400_BAD_REQUEST) + + task.status = 'running' + task.started_at = timezone.now() + task.save() + + return Response(TaskSerializer(task).data, status=status.HTTP_200_OK) + + @action(detail=True, methods=['post'], serializer_class=TaskCompleteSerializer) + def complete(self, request, pk=None): + """Client completes a task""" + task = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + if task.status != 'running': + return Response({'detail': 'Task is not running'}, status=status.HTTP_400_BAD_REQUEST) + + task.status = serializer.validated_data['status'] + task.completed_at = timezone.now() + task.save() + + return Response(TaskSerializer(task).data, status=status.HTTP_200_OK) + +class TaskResultViewSet(viewsets.ModelViewSet): + queryset = TaskResult.objects.all() + serializer_class = TaskResultSerializer + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser] + + @action(detail=True, methods=['get']) + def download(self, request, pk=None): + """Download task result file""" + task_result = self.get_object() + if not task_result.result_file: + return Response({'detail': 'No file available'}, status=status.HTTP_404_NOT_FOUND) + + # Use Django's built-in FileResponse for downloading + from django.http import FileResponse + return FileResponse(task_result.result_file.open('rb'), as_attachment=True) + diff --git a/tasks/views_frontend.py b/tasks/views_frontend.py new file mode 100644 index 0000000..137e5bc --- /dev/null +++ b/tasks/views_frontend.py @@ -0,0 +1,54 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib import messages +from .models import Task, Client, TaskResult + +# Home view +def index(request): + return render(request, 'tasks/index.html') + +# Task views +def task_list(request): + tasks = Task.objects.all() + return render(request, 'tasks/task_list.html', {'tasks': tasks}) + +def task_create(request): + if request.method == 'POST': + name = request.POST['name'] + client_name = request.POST.get('client_name', '') + script = request.POST.get('script', '') + timeout_seconds = int(request.POST.get('timeout_seconds', 259200)) + + Task.objects.create( + name=name, + client_name=client_name if client_name else None, + script=script if script else None, + timeout_seconds=timeout_seconds + ) + messages.success(request, '任务创建成功!') + return redirect('task_list') + return render(request, 'tasks/task_create.html') + +def task_detail(request, task_id): + task = get_object_or_404(Task, id=task_id) + results = task.results.all() + return render(request, 'tasks/task_detail.html', {'task': task, 'results': results}) + +# Client views +def client_list(request): + clients = Client.objects.all() + return render(request, 'tasks/client_list.html', {'clients': clients}) + +def client_create(request): + if request.method == 'POST': + name = request.POST['name'] + # Generate a simple token for demo purposes + import secrets + token = secrets.token_urlsafe(32) + + Client.objects.create( + name=name, + token=token + ) + messages.success(request, '客户端创建成功!') + return redirect('client_list') + return render(request, 'tasks/client_create.html')