cacode commited on
Commit
4fb3744
·
verified ·
1 Parent(s): 28da773

Upload 67 files

Browse files
app/__pycache__/models.cpython-313.pyc CHANGED
Binary files a/app/__pycache__/models.cpython-313.pyc and b/app/__pycache__/models.cpython-313.pyc differ
 
app/models.py CHANGED
@@ -72,6 +72,7 @@ class Activity(TimestampMixin, Base):
72
  description: Mapped[str] = mapped_column(Text, default="")
73
  start_at: Mapped[datetime] = mapped_column(DateTime)
74
  deadline_at: Mapped[datetime] = mapped_column(DateTime)
 
75
  leaderboard_visible: Mapped[bool] = mapped_column(Boolean, default=True)
76
  clue_interval_minutes: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
77
  created_by_id: Mapped[int] = mapped_column(ForeignKey("admins.id"))
@@ -150,3 +151,4 @@ class Submission(TimestampMixin, Base):
150
  back_populates="assigned_submissions",
151
  foreign_keys=[assigned_admin_id],
152
  )
 
 
72
  description: Mapped[str] = mapped_column(Text, default="")
73
  start_at: Mapped[datetime] = mapped_column(DateTime)
74
  deadline_at: Mapped[datetime] = mapped_column(DateTime)
75
+ is_visible: Mapped[bool] = mapped_column(Boolean, default=True)
76
  leaderboard_visible: Mapped[bool] = mapped_column(Boolean, default=True)
77
  clue_interval_minutes: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
78
  created_by_id: Mapped[int] = mapped_column(ForeignKey("admins.id"))
 
151
  back_populates="assigned_submissions",
152
  foreign_keys=[assigned_admin_id],
153
  )
154
+
app/routes/__pycache__/admin.cpython-313.pyc CHANGED
Binary files a/app/routes/__pycache__/admin.cpython-313.pyc and b/app/routes/__pycache__/admin.cpython-313.pyc differ
 
app/routes/__pycache__/user.cpython-313.pyc CHANGED
Binary files a/app/routes/__pycache__/user.cpython-313.pyc and b/app/routes/__pycache__/user.cpython-313.pyc differ
 
app/routes/admin.py CHANGED
@@ -91,6 +91,7 @@ def parse_activity_fields(form) -> dict:
91
  start_raw = str(form.get("start_at", "")).strip()
92
  deadline_raw = str(form.get("deadline_at", "")).strip()
93
  clue_interval_raw = str(form.get("clue_interval_minutes", "")).strip()
 
94
  leaderboard_visible = form.get("leaderboard_visible") == "on"
95
 
96
  if not title or not start_raw or not deadline_raw:
@@ -116,6 +117,7 @@ def parse_activity_fields(form) -> dict:
116
  "start_at": start_at,
117
  "deadline_at": deadline_at,
118
  "clue_interval_minutes": clue_interval_minutes,
 
119
  "leaderboard_visible": leaderboard_visible,
120
  }
121
 
@@ -191,6 +193,14 @@ def cleanup_task_files(task: Task) -> None:
191
  delete_file_if_exists(task.clue_image_path, settings.task_media_root)
192
 
193
 
 
 
 
 
 
 
 
 
194
  def normalize_path_key(file_path: str | Path) -> str:
195
  return str(Path(file_path).resolve())
196
 
@@ -1087,8 +1097,31 @@ async def delete_task(task_id: int, request: Request, db: Session = Depends(get_
1087
  add_flash(request, "success", "任务已删除,剩余任务顺序和线索发布时间已自动更新。")
1088
  return redirect(f"/admin/activities/{activity_id}/edit")
1089
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1090
  @router.post("/admin/activities/{activity_id}/visibility")
1091
- async def update_leaderboard_visibility(activity_id: int, request: Request, db: Session = Depends(get_db)):
1092
  admin = require_admin(request, db)
1093
  if not admin:
1094
  return redirect("/admin")
@@ -1098,10 +1131,11 @@ async def update_leaderboard_visibility(activity_id: int, request: Request, db:
1098
  raise HTTPException(status_code=404, detail="活动不存在")
1099
 
1100
  form = await request.form()
 
1101
  activity.leaderboard_visible = form.get("leaderboard_visible") == "on"
1102
  db.add(activity)
1103
  db.commit()
1104
- add_flash(request, "success", f"已更新 {activity.title} 的排行榜可见性。")
1105
  return redirect("/admin/activities")
1106
 
1107
  @router.get("/admin/reviews")
@@ -1339,3 +1373,7 @@ async def delete_managed_image(request: Request, db: Session = Depends(get_db)):
1339
 
1340
 
1341
 
 
 
 
 
 
91
  start_raw = str(form.get("start_at", "")).strip()
92
  deadline_raw = str(form.get("deadline_at", "")).strip()
93
  clue_interval_raw = str(form.get("clue_interval_minutes", "")).strip()
94
+ is_visible = form.get("is_visible") == "on"
95
  leaderboard_visible = form.get("leaderboard_visible") == "on"
96
 
97
  if not title or not start_raw or not deadline_raw:
 
117
  "start_at": start_at,
118
  "deadline_at": deadline_at,
119
  "clue_interval_minutes": clue_interval_minutes,
120
+ "is_visible": is_visible,
121
  "leaderboard_visible": leaderboard_visible,
122
  }
123
 
 
193
  delete_file_if_exists(task.clue_image_path, settings.task_media_root)
194
 
195
 
196
+ def cleanup_activity_files(activity: Activity) -> None:
197
+ all_submissions: list[Submission] = []
198
+ for task in activity.tasks:
199
+ all_submissions.extend(task.submissions)
200
+ cleanup_task_files(task)
201
+ cleanup_submission_files(all_submissions)
202
+
203
+
204
  def normalize_path_key(file_path: str | Path) -> str:
205
  return str(Path(file_path).resolve())
206
 
 
1097
  add_flash(request, "success", "任务已删除,剩余任务顺序和线索发布时间已自动更新。")
1098
  return redirect(f"/admin/activities/{activity_id}/edit")
1099
 
1100
+ @router.post("/admin/activities/{activity_id}/delete")
1101
+ async def delete_activity(activity_id: int, request: Request, db: Session = Depends(get_db)):
1102
+ admin = require_admin(request, db)
1103
+ if not admin:
1104
+ return redirect("/admin")
1105
+
1106
+ activity = (
1107
+ db.query(Activity)
1108
+ .options(joinedload(Activity.tasks).joinedload(Task.submissions))
1109
+ .filter(Activity.id == activity_id)
1110
+ .first()
1111
+ )
1112
+ if not activity:
1113
+ raise HTTPException(status_code=404, detail="活动不存在")
1114
+
1115
+ activity_title = activity.title
1116
+ cleanup_activity_files(activity)
1117
+ db.delete(activity)
1118
+ db.commit()
1119
+ add_flash(request, "success", f"活动 {activity_title} 已删除,相关任务与本地提交图片也已清理。")
1120
+ return redirect("/admin/activities")
1121
+
1122
+
1123
  @router.post("/admin/activities/{activity_id}/visibility")
1124
+ async def update_activity_visibility(activity_id: int, request: Request, db: Session = Depends(get_db)):
1125
  admin = require_admin(request, db)
1126
  if not admin:
1127
  return redirect("/admin")
 
1131
  raise HTTPException(status_code=404, detail="活动不存在")
1132
 
1133
  form = await request.form()
1134
+ activity.is_visible = form.get("is_visible") == "on"
1135
  activity.leaderboard_visible = form.get("leaderboard_visible") == "on"
1136
  db.add(activity)
1137
  db.commit()
1138
+ add_flash(request, "success", f"已更新 {activity.title} 的活动可见性和排行榜设置。")
1139
  return redirect("/admin/activities")
1140
 
1141
  @router.get("/admin/reviews")
 
1373
 
1374
 
1375
 
1376
+
1377
+
1378
+
1379
+
app/routes/user.py CHANGED
@@ -45,6 +45,7 @@ def dashboard(request: Request, db: Session = Depends(get_db)):
45
  activities = (
46
  db.query(Activity)
47
  .options(joinedload(Activity.tasks).joinedload(Task.submissions))
 
48
  .order_by(Activity.start_at.asc())
49
  .all()
50
  )
@@ -85,6 +86,9 @@ def activity_detail(activity_id: int, request: Request, db: Session = Depends(ge
85
  activity = get_activity_with_group_context(db, activity_id)
86
  if not activity:
87
  raise HTTPException(status_code=404, detail="活动不存在")
 
 
 
88
 
89
  now = local_now()
90
  group_submission_by_task = build_group_submission_map(activity, user.group_id)
@@ -128,8 +132,12 @@ async def submit_task(
128
  return redirect(f"/activities/{activity_id}")
129
 
130
  activity = db.query(Activity).filter(Activity.id == activity_id).first()
 
 
 
 
131
  task = db.query(Task).filter(Task.id == task_id, Task.activity_id == activity_id).first()
132
- if not activity or not task:
133
  raise HTTPException(status_code=404, detail="任务不存在")
134
 
135
  now = local_now()
@@ -203,7 +211,7 @@ def activity_clues(activity_id: int, request: Request, db: Session = Depends(get
203
  return JSONResponse({"error": "unauthorized"}, status_code=401)
204
 
205
  activity = db.query(Activity).options(joinedload(Activity.tasks)).filter(Activity.id == activity_id).first()
206
- if not activity:
207
  return JSONResponse({"error": "not_found"}, status_code=404)
208
 
209
  now = local_now()
@@ -226,7 +234,7 @@ def activity_status(activity_id: int, request: Request, db: Session = Depends(ge
226
  return JSONResponse({"error": "unauthorized"}, status_code=401)
227
 
228
  activity = get_activity_with_group_context(db, activity_id)
229
- if not activity:
230
  return JSONResponse({"error": "not_found"}, status_code=404)
231
 
232
  submission_map = build_group_submission_map(activity, user.group_id)
@@ -275,3 +283,8 @@ def activity_status(activity_id: int, request: Request, db: Session = Depends(ge
275
 
276
 
277
 
 
 
 
 
 
 
45
  activities = (
46
  db.query(Activity)
47
  .options(joinedload(Activity.tasks).joinedload(Task.submissions))
48
+ .filter(Activity.is_visible.is_(True))
49
  .order_by(Activity.start_at.asc())
50
  .all()
51
  )
 
86
  activity = get_activity_with_group_context(db, activity_id)
87
  if not activity:
88
  raise HTTPException(status_code=404, detail="活动不存在")
89
+ if not activity.is_visible:
90
+ add_flash(request, "error", "该活动当前暂未对用户开放。")
91
+ return redirect("/dashboard")
92
 
93
  now = local_now()
94
  group_submission_by_task = build_group_submission_map(activity, user.group_id)
 
132
  return redirect(f"/activities/{activity_id}")
133
 
134
  activity = db.query(Activity).filter(Activity.id == activity_id).first()
135
+ if not activity or not activity.is_visible:
136
+ add_flash(request, "error", "该活动当前暂未对用户开放。")
137
+ return redirect("/dashboard")
138
+
139
  task = db.query(Task).filter(Task.id == task_id, Task.activity_id == activity_id).first()
140
+ if not task:
141
  raise HTTPException(status_code=404, detail="任务不存在")
142
 
143
  now = local_now()
 
211
  return JSONResponse({"error": "unauthorized"}, status_code=401)
212
 
213
  activity = db.query(Activity).options(joinedload(Activity.tasks)).filter(Activity.id == activity_id).first()
214
+ if not activity or not activity.is_visible:
215
  return JSONResponse({"error": "not_found"}, status_code=404)
216
 
217
  now = local_now()
 
234
  return JSONResponse({"error": "unauthorized"}, status_code=401)
235
 
236
  activity = get_activity_with_group_context(db, activity_id)
237
+ if not activity or not activity.is_visible:
238
  return JSONResponse({"error": "not_found"}, status_code=404)
239
 
240
  submission_map = build_group_submission_map(activity, user.group_id)
 
283
 
284
 
285
 
286
+
287
+
288
+
289
+
290
+
app/services/__pycache__/bootstrap.cpython-313.pyc CHANGED
Binary files a/app/services/__pycache__/bootstrap.cpython-313.pyc and b/app/services/__pycache__/bootstrap.cpython-313.pyc differ
 
app/services/bootstrap.py CHANGED
@@ -148,6 +148,7 @@ def migrate_task_media_to_files() -> None:
148
  def upgrade_schema() -> None:
149
  ensure_column("admins", "last_seen_at", "last_seen_at DATETIME NULL")
150
  ensure_column("users", "last_seen_at", "last_seen_at DATETIME NULL")
 
151
  ensure_column("activities", "leaderboard_visible", "leaderboard_visible TINYINT(1) NOT NULL DEFAULT 1")
152
  ensure_column("activities", "clue_interval_minutes", "clue_interval_minutes INT NULL")
153
  ensure_column("tasks", "display_order", "display_order INT NOT NULL DEFAULT 1")
@@ -231,3 +232,4 @@ def seed_super_admin(db: Session) -> None:
231
 
232
 
233
 
 
 
148
  def upgrade_schema() -> None:
149
  ensure_column("admins", "last_seen_at", "last_seen_at DATETIME NULL")
150
  ensure_column("users", "last_seen_at", "last_seen_at DATETIME NULL")
151
+ ensure_column("activities", "is_visible", "is_visible TINYINT(1) NOT NULL DEFAULT 1")
152
  ensure_column("activities", "leaderboard_visible", "leaderboard_visible TINYINT(1) NOT NULL DEFAULT 1")
153
  ensure_column("activities", "clue_interval_minutes", "clue_interval_minutes INT NULL")
154
  ensure_column("tasks", "display_order", "display_order INT NOT NULL DEFAULT 1")
 
232
 
233
 
234
 
235
+
app/templates/admin_activities.html CHANGED
@@ -32,6 +32,10 @@
32
  <span>活动说明</span>
33
  <textarea name="description" rows="3" placeholder="介绍活动安排、打卡要求和注意事项"></textarea>
34
  </label>
 
 
 
 
35
  <label class="checkbox-row">
36
  <input type="checkbox" name="leaderboard_visible" checked />
37
  <span>允许用户查看实时排行榜</span>
@@ -91,16 +95,27 @@
91
  <p class="muted">{{ activity.start_at|datetime_local }} 至 {{ activity.deadline_at|datetime_local }}</p>
92
  <p class="muted">{{ activity.tasks|length }} 个任务 · 创建人 {{ activity.created_by.display_name }}</p>
93
  <p class="muted">线索间隔:{{ activity.clue_interval_minutes if activity.clue_interval_minutes is not none else '与活动开始同步' }}</p>
 
 
 
 
94
  </div>
95
  <div class="activity-actions">
96
  <a class="btn btn-primary small-btn" href="/admin/activities/{{ activity.id }}/edit">编辑活动</a>
97
  <form method="post" action="/admin/activities/{{ activity.id }}/visibility" class="inline-form visibility-form">
 
 
 
 
98
  <label class="checkbox-row compact-checkbox">
99
  <input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} />
100
  <span>用户可见排行榜</span>
101
  </label>
102
  <button class="btn btn-secondary small-btn" type="submit">保存</button>
103
  </form>
 
 
 
104
  </div>
105
  </article>
106
  {% else %}
@@ -139,7 +154,11 @@
139
  const template = builder.querySelector('[data-task-template]');
140
  const clone = template.cloneNode(true);
141
  clone.querySelectorAll('input, textarea').forEach((field) => {
142
- field.value = '';
 
 
 
 
143
  });
144
  attachRemove(clone);
145
  builder.appendChild(clone);
@@ -148,4 +167,3 @@
148
  })();
149
  </script>
150
  {% endblock %}
151
-
 
32
  <span>活动说明</span>
33
  <textarea name="description" rows="3" placeholder="介绍活动安排、打卡要求和注意事项"></textarea>
34
  </label>
35
+ <label class="checkbox-row">
36
+ <input type="checkbox" name="is_visible" checked />
37
+ <span>允许用户查看该活动</span>
38
+ </label>
39
  <label class="checkbox-row">
40
  <input type="checkbox" name="leaderboard_visible" checked />
41
  <span>允许用户查看实时排行榜</span>
 
95
  <p class="muted">{{ activity.start_at|datetime_local }} 至 {{ activity.deadline_at|datetime_local }}</p>
96
  <p class="muted">{{ activity.tasks|length }} 个任务 · 创建人 {{ activity.created_by.display_name }}</p>
97
  <p class="muted">线索间隔:{{ activity.clue_interval_minutes if activity.clue_interval_minutes is not none else '与活动开始同步' }}</p>
98
+ <div class="chip-row">
99
+ <span class="chip">{{ '用户端可见' if activity.is_visible else '用户端隐藏' }}</span>
100
+ <span class="chip">{{ '排行榜可见' if activity.leaderboard_visible else '排行榜隐藏' }}</span>
101
+ </div>
102
  </div>
103
  <div class="activity-actions">
104
  <a class="btn btn-primary small-btn" href="/admin/activities/{{ activity.id }}/edit">编辑活动</a>
105
  <form method="post" action="/admin/activities/{{ activity.id }}/visibility" class="inline-form visibility-form">
106
+ <label class="checkbox-row compact-checkbox">
107
+ <input type="checkbox" name="is_visible" {% if activity.is_visible %}checked{% endif %} />
108
+ <span>用户可见活动</span>
109
+ </label>
110
  <label class="checkbox-row compact-checkbox">
111
  <input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} />
112
  <span>用户可见排行榜</span>
113
  </label>
114
  <button class="btn btn-secondary small-btn" type="submit">保存</button>
115
  </form>
116
+ <form method="post" action="/admin/activities/{{ activity.id }}/delete" class="inline-form">
117
+ <button class="btn btn-danger small-btn" type="submit" onclick="return confirm('确定删除这个活动吗?相关任务、审核记录和本地提交图片都会一并删除。');">删除活动</button>
118
+ </form>
119
  </div>
120
  </article>
121
  {% else %}
 
154
  const template = builder.querySelector('[data-task-template]');
155
  const clone = template.cloneNode(true);
156
  clone.querySelectorAll('input, textarea').forEach((field) => {
157
+ if (field.type === 'checkbox') {
158
+ field.checked = false;
159
+ } else {
160
+ field.value = '';
161
+ }
162
  });
163
  attachRemove(clone);
164
  builder.appendChild(clone);
 
167
  })();
168
  </script>
169
  {% endblock %}
 
app/templates/admin_activity_edit.html CHANGED
@@ -12,6 +12,10 @@
12
  <span class="pill">创建人 {{ activity.created_by.display_name }}</span>
13
  <span class="pill">{{ activity.tasks|length }} 个任务</span>
14
  <span class="pill">开始 {{ activity.start_at|datetime_local }}</span>
 
 
 
 
15
  </div>
16
  </section>
17
 
@@ -45,6 +49,10 @@
45
  <span>活动说明</span>
46
  <textarea name="description" rows="3">{{ activity.description or '' }}</textarea>
47
  </label>
 
 
 
 
48
  <label class="checkbox-row">
49
  <input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} />
50
  <span>允许用户查看实时排行榜</span>
@@ -59,7 +67,8 @@
59
  <p class="mini-note">管理员始终可见,用于现场统筹和审核判断;用户是否可见由上方开关控制。</p>
60
  </div>
61
  <div class="chip-row">
62
- <span class="chip">用户端{{ '可见' if activity.leaderboard_visible else '隐藏' }}</span>
 
63
  <span class="chip">按完成点位数优先,再按总耗时排序</span>
64
  </div>
65
  </div>
@@ -206,6 +215,8 @@
206
  </section>
207
  </form>
208
 
 
 
209
  {% for task in activity.tasks %}
210
  <form id="delete-task-{{ task.id }}" method="post" action="/admin/tasks/{{ task.id }}/delete"></form>
211
  {% endfor %}
@@ -254,3 +265,7 @@
254
  </script>
255
  {% endblock %}
256
 
 
 
 
 
 
12
  <span class="pill">创建人 {{ activity.created_by.display_name }}</span>
13
  <span class="pill">{{ activity.tasks|length }} 个任务</span>
14
  <span class="pill">开始 {{ activity.start_at|datetime_local }}</span>
15
+ <span class="pill">{{ '用户端可见' if activity.is_visible else '用户端隐藏' }}</span>
16
+ </div>
17
+ <div class="card-footer">
18
+ <button class="btn btn-danger" type="submit" form="delete-activity-form" onclick="return confirm('确定删除这个活动吗?相关任务、审核记录和本地提交图片都会一并删除。');">删除整个活动</button>
19
  </div>
20
  </section>
21
 
 
49
  <span>活动说明</span>
50
  <textarea name="description" rows="3">{{ activity.description or '' }}</textarea>
51
  </label>
52
+ <label class="checkbox-row">
53
+ <input type="checkbox" name="is_visible" {% if activity.is_visible %}checked{% endif %} />
54
+ <span>允许用户查看该活动</span>
55
+ </label>
56
  <label class="checkbox-row">
57
  <input type="checkbox" name="leaderboard_visible" {% if activity.leaderboard_visible %}checked{% endif %} />
58
  <span>允许用户查看实时排行榜</span>
 
67
  <p class="mini-note">管理员始终可见,用于现场统筹和审核判断;用户是否可见由上方开关控制。</p>
68
  </div>
69
  <div class="chip-row">
70
+ <span class="chip">活动{{ '可见' if activity.is_visible else '隐藏' }}</span>
71
+ <span class="chip">排行榜{{ '可见' if activity.leaderboard_visible else '隐藏' }}</span>
72
  <span class="chip">按完成点位数优先,再按总耗时排序</span>
73
  </div>
74
  </div>
 
215
  </section>
216
  </form>
217
 
218
+ <form id="delete-activity-form" method="post" action="/admin/activities/{{ activity.id }}/delete"></form>
219
+
220
  {% for task in activity.tasks %}
221
  <form id="delete-task-{{ task.id }}" method="post" action="/admin/tasks/{{ task.id }}/delete"></form>
222
  {% endfor %}
 
265
  </script>
266
  {% endblock %}
267
 
268
+
269
+
270
+
271
+
app/templates/admin_dashboard.html CHANGED
@@ -62,8 +62,9 @@
62
  <div>
63
  <strong>{{ activity.title }}</strong>
64
  <p class="muted">{{ activity.tasks|length }} 个任务 · {{ activity.start_at|datetime_local }}</p>
 
65
  </div>
66
- <span class="status-badge">{{ '排行榜可见' if activity.leaderboard_visible else '排行榜隐藏' }}</span>
67
  </div>
68
  {% else %}
69
  <p class="muted">还没有发布活动。</p>
@@ -116,3 +117,4 @@
116
  </section>
117
  {% endif %}
118
  {% endblock %}
 
 
62
  <div>
63
  <strong>{{ activity.title }}</strong>
64
  <p class="muted">{{ activity.tasks|length }} 个任务 · {{ activity.start_at|datetime_local }}</p>
65
+ <p class="muted">{{ '用户端可见' if activity.is_visible else '用户端隐藏' }} · {{ '排行榜可见' if activity.leaderboard_visible else '排行榜隐藏' }}</p>
66
  </div>
67
+ <span class="status-badge {% if activity.is_visible %}status-approved{% else %}status-rejected{% endif %}">{{ '活动可见' if activity.is_visible else '活动隐藏' }}</span>
68
  </div>
69
  {% else %}
70
  <p class="muted">还没有发布活动。</p>
 
117
  </section>
118
  {% endif %}
119
  {% endblock %}
120
+