更新学期功能
This commit is contained in:
@@ -137,6 +137,73 @@ class SemesterModel:
|
||||
GROUP BY hs.student_id, hs.status
|
||||
"""
|
||||
return await execute_query(sql, (start_date, end_date))
|
||||
|
||||
@staticmethod
|
||||
async def update(
|
||||
semester_id: int,
|
||||
semester_name: str = None,
|
||||
start_date: str = None,
|
||||
end_date: str = None
|
||||
) -> bool:
|
||||
"""编辑学期信息(仅未归档)"""
|
||||
sets = []
|
||||
params = []
|
||||
if semester_name is not None:
|
||||
sets.append("semester_name = %s")
|
||||
params.append(semester_name)
|
||||
if start_date is not None:
|
||||
sets.append("start_date = %s")
|
||||
params.append(start_date)
|
||||
if end_date is not None:
|
||||
sets.append("end_date = %s")
|
||||
params.append(end_date)
|
||||
if not sets:
|
||||
return False
|
||||
params.append(semester_id)
|
||||
sql = f"UPDATE semesters SET {', '.join(sets)} WHERE semester_id = %s AND is_archived = 0"
|
||||
result = await execute_update(sql, tuple(params))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def delete(semester_id: int) -> bool:
|
||||
"""删除学期"""
|
||||
sql = "DELETE FROM semesters WHERE semester_id = %s"
|
||||
result = await execute_update(sql, (semester_id,))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def count_archives(semester_id: int) -> int:
|
||||
"""统计学期的归档数据数量"""
|
||||
sql = "SELECT COUNT(*) as cnt FROM semester_archives WHERE semester_id = %s"
|
||||
result = await execute_one(sql, (semester_id,))
|
||||
return result['cnt'] if result else 0
|
||||
|
||||
@staticmethod
|
||||
async def associate_records_by_date_range(
|
||||
semester_id: int,
|
||||
start_date: str,
|
||||
end_date: str
|
||||
) -> Dict[str, int]:
|
||||
"""按日期范围关联记录到学期"""
|
||||
# 关联操行分记录(created_at 为 TIMESTAMP,需包含 end_date 当天)
|
||||
conduct_sql = """
|
||||
UPDATE conduct_records
|
||||
SET semester_id = %s
|
||||
WHERE semester_id IS NULL
|
||||
AND created_at BETWEEN %s AND CONCAT(%s, ' 23:59:59')
|
||||
"""
|
||||
conduct_count = await execute_update(conduct_sql, (semester_id, start_date, end_date))
|
||||
|
||||
# 关联考勤记录
|
||||
attendance_sql = """
|
||||
UPDATE attendance_records
|
||||
SET semester_id = %s
|
||||
WHERE semester_id IS NULL
|
||||
AND `date` BETWEEN %s AND %s
|
||||
"""
|
||||
attendance_count = await execute_update(attendance_sql, (semester_id, start_date, end_date))
|
||||
|
||||
return {"conduct": conduct_count, "attendance": attendance_count}
|
||||
|
||||
|
||||
class SemesterArchiveModel:
|
||||
|
||||
@@ -18,7 +18,7 @@ from middleware.permission import (
|
||||
)
|
||||
from services.semester_service import SemesterService
|
||||
from services.log_service import LogService
|
||||
from schemas.semester import CreateSemesterRequest
|
||||
from schemas.semester import CreateSemesterRequest, UpdateSemesterRequest
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
@@ -103,8 +103,90 @@ async def activate_semester(request: Request, semester_id: int):
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.put("/update/{semester_id}")
|
||||
async def update_semester(request: Request, semester_id: int, req: UpdateSemesterRequest):
|
||||
"""编辑学期(班主任)"""
|
||||
user = await get_current_user(request)
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可编辑学期", code=403)
|
||||
|
||||
result = await SemesterService.update_semester(
|
||||
semester_id=semester_id,
|
||||
semester_name=req.semester_name,
|
||||
start_date=req.start_date,
|
||||
end_date=req.end_date,
|
||||
operator_id=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="update_semester",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"编辑学期ID: {semester_id}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message=result["message"])
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.delete("/delete/{semester_id}")
|
||||
async def delete_semester(request: Request, semester_id: int):
|
||||
"""删除学期(班主任)"""
|
||||
user = await get_current_user(request)
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可删除学期", code=403)
|
||||
|
||||
result = await SemesterService.delete_semester(
|
||||
semester_id=semester_id,
|
||||
operator_id=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="delete_semester",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"删除学期ID: {semester_id}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message=result["message"])
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.post("/{semester_id}/associate")
|
||||
async def associate_records(request: Request, semester_id: int):
|
||||
"""关联记录到学期(班主任)"""
|
||||
user = await get_current_user(request)
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可关联数据", code=403)
|
||||
|
||||
result = await SemesterService.associate_records(
|
||||
semester_id=semester_id,
|
||||
operator_id=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="associate_records",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"关联数据到学期ID: {semester_id}, 结果: {result.get('data', {})}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(data=result.get("data"), message=result["message"])
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.post("/archive/{semester_id}")
|
||||
async def archive_semester(request: Request, semester_id: int):
|
||||
async def archive_semester(
|
||||
request: Request,
|
||||
semester_id: int,
|
||||
reset_scores: bool = Query(False)
|
||||
):
|
||||
"""归档学期(班主任)"""
|
||||
user = await get_current_user(request)
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
@@ -113,14 +195,18 @@ async def archive_semester(request: Request, semester_id: int):
|
||||
|
||||
result = await SemesterService.archive_semester(
|
||||
semester_id=semester_id,
|
||||
operator_id=user["user_id"]
|
||||
operator_id=user["user_id"],
|
||||
reset_scores=reset_scores
|
||||
)
|
||||
if result["success"]:
|
||||
log_detail = f"归档学期ID: {semester_id}"
|
||||
if reset_scores:
|
||||
log_detail += " 并重置学生操行分"
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="archive_semester",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"归档学期ID: {semester_id}",
|
||||
details=log_detail,
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message=result["message"])
|
||||
|
||||
@@ -18,3 +18,10 @@ class CreateSemesterRequest(BaseModel):
|
||||
semester_name: str = Field(..., min_length=1, max_length=100, description="学期名称")
|
||||
start_date: Optional[str] = Field(None, description="学期开始日期 (YYYY-MM-DD)")
|
||||
end_date: Optional[str] = Field(None, description="学期结束日期 (YYYY-MM-DD)")
|
||||
|
||||
|
||||
class UpdateSemesterRequest(BaseModel):
|
||||
"""编辑学期请求"""
|
||||
semester_name: Optional[str] = Field(None, min_length=1, max_length=100, description="学期名称")
|
||||
start_date: Optional[str] = Field(None, description="学期开始日期 (YYYY-MM-DD)")
|
||||
end_date: Optional[str] = Field(None, description="学期结束日期 (YYYY-MM-DD)")
|
||||
|
||||
@@ -127,9 +127,110 @@ class SemesterService:
|
||||
return {"success": False, "message": f"激活学期失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def archive_semester(
|
||||
async def update_semester(
|
||||
semester_id: int,
|
||||
semester_name: str = None,
|
||||
start_date: str = None,
|
||||
end_date: str = None,
|
||||
operator_id: int = None
|
||||
) -> Dict[str, Any]:
|
||||
"""编辑学期信息"""
|
||||
try:
|
||||
semester = await SemesterModel.get_by_id(semester_id)
|
||||
if not semester:
|
||||
return {"success": False, "message": "学期不存在"}
|
||||
|
||||
if semester['is_archived']:
|
||||
return {"success": False, "message": "已归档的学期不能编辑"}
|
||||
|
||||
result = await SemesterModel.update(
|
||||
semester_id=semester_id,
|
||||
semester_name=semester_name,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info(f"用户[{operator_id}] 编辑了学期: {semester['semester_name']}")
|
||||
return {"success": True, "message": "学期信息已更新"}
|
||||
else:
|
||||
return {"success": False, "message": "更新失败,请检查参数"}
|
||||
except Exception as e:
|
||||
logger.error(f"编辑学期失败: {e}")
|
||||
return {"success": False, "message": f"编辑学期失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def delete_semester(
|
||||
semester_id: int,
|
||||
operator_id: int = None
|
||||
) -> Dict[str, Any]:
|
||||
"""删除学期"""
|
||||
try:
|
||||
semester = await SemesterModel.get_by_id(semester_id)
|
||||
if not semester:
|
||||
return {"success": False, "message": "学期不存在"}
|
||||
|
||||
# 检查是否有关联归档数据
|
||||
archive_count = await SemesterModel.count_archives(semester_id)
|
||||
if archive_count > 0:
|
||||
return {"success": False, "message": f"该学期有 {archive_count} 条归档数据,无法删除"}
|
||||
|
||||
result = await SemesterModel.delete(semester_id)
|
||||
|
||||
if result:
|
||||
logger.info(f"用户[{operator_id}] 删除了学期: {semester['semester_name']}")
|
||||
return {"success": True, "message": "学期已删除"}
|
||||
else:
|
||||
return {"success": False, "message": "删除失败"}
|
||||
except Exception as e:
|
||||
logger.error(f"删除学期失败: {e}")
|
||||
return {"success": False, "message": f"删除学期失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def associate_records(
|
||||
semester_id: int,
|
||||
operator_id: int = None
|
||||
) -> Dict[str, Any]:
|
||||
"""关联记录到学期"""
|
||||
try:
|
||||
semester = await SemesterModel.get_by_id(semester_id)
|
||||
if not semester:
|
||||
return {"success": False, "message": "学期不存在"}
|
||||
|
||||
if semester['is_archived']:
|
||||
return {"success": False, "message": "已归档的学期不能关联数据"}
|
||||
|
||||
start_date = semester.get('start_date')
|
||||
if not start_date:
|
||||
return {"success": False, "message": "学期未设置开始日期,无法关联数据"}
|
||||
|
||||
end_date = semester.get('end_date') or datetime.date.today().isoformat()
|
||||
|
||||
counts = await SemesterModel.associate_records_by_date_range(
|
||||
semester_id=semester_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"用户[{operator_id}] 关联数据到学期: {semester['semester_name']}, "
|
||||
f"操行分 {counts['conduct']} 条, 考勤 {counts['attendance']} 条"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"关联完成:操行分 {counts['conduct']} 条,考勤 {counts['attendance']} 条",
|
||||
"data": counts
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"关联记录失败: {e}")
|
||||
return {"success": False, "message": f"关联记录失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def archive_semester(
|
||||
semester_id: int,
|
||||
operator_id: int = None,
|
||||
reset_scores: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""归档学期"""
|
||||
try:
|
||||
@@ -231,6 +332,18 @@ class SemesterService:
|
||||
# 标记学期为已归档
|
||||
await SemesterModel.archive(semester_id)
|
||||
|
||||
# 归档成功后按需重置学生操行分
|
||||
if reset_scores:
|
||||
reset_result = await SemesterService.reset_student_points()
|
||||
logger.info(
|
||||
f"用户[{operator_id}] 归档学期: {semester['semester_name']} 并重置学生操行分, "
|
||||
f"共 {total_students} 名学生"
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"学期归档成功,共归档 {total_students} 名学生数据,已重置学生操行分"
|
||||
}
|
||||
|
||||
logger.info(
|
||||
f"用户[{operator_id}] 归档了学期: {semester['semester_name']}, "
|
||||
f"共 {total_students} 名学生"
|
||||
|
||||
@@ -84,6 +84,54 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑学期模态框 -->
|
||||
<div id="editSemesterModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>编辑学期</h3>
|
||||
<button class="modal-close" onclick="closeModal('editSemesterModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitEditSemester()">
|
||||
<input type="hidden" id="editSemesterId">
|
||||
<div class="form-group">
|
||||
<label>学期名称 <span style="color:red;">*</span></label>
|
||||
<input type="text" id="editSemesterName" required placeholder="如:2025春季学期" maxlength="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>开始日期</label>
|
||||
<input type="date" id="editSemesterStartDate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>结束日期 <small style="color: #999;">(可选)</small></label>
|
||||
<input type="date" id="editSemesterEndDate">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">保存修改</button>
|
||||
<button type="button" class="btn btn-danger" onclick="deleteSemester()">删除学期</button>
|
||||
<button type="button" class="btn" onclick="closeModal('editSemesterModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联数据确认模态框 -->
|
||||
<div id="associateConfirmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>关联数据到学期</h3>
|
||||
<button class="modal-close" onclick="closeModal('associateConfirmModal')">×</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p id="associateConfirmText" style="margin: 10px 0;"></p>
|
||||
<p style="color: #666; font-size: 14px;">将把该日期范围内所有未分配学期的操行分记录和考勤记录关联到此学期。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="confirmAssociate()">确认关联</button>
|
||||
<button type="button" class="btn" onclick="closeModal('associateConfirmModal')">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 归档确认模态框 -->
|
||||
<div id="archiveConfirmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
@@ -93,7 +141,14 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p id="archiveConfirmText" style="margin: 10px 0;"></p>
|
||||
<p style="color: #e74c3c; font-size: 14px;">注意:归档前需确保学期已设置开始日期,否则无法归档。归档后该学期的操行分记录将不可修改或撤销,但可以查看归档数据。</p>
|
||||
<p style="color: #666; font-size: 14px;">归档会创建所有学生当前操行分的数据快照,原始数据不受影响。</p>
|
||||
<div style="margin-top: 10px;">
|
||||
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 14px;">
|
||||
<input type="checkbox" id="archiveResetScores">
|
||||
归档后重置所有学生操行分为初始值(60分)
|
||||
</label>
|
||||
</div>
|
||||
<p style="color: #e74c3c; font-size: 13px; margin-top: 6px;">注意:归档前需确保学期已设置开始日期,否则无法归档。归档后该学期数据将变为只读,不可撤销。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="confirmArchive()">确认归档</button>
|
||||
@@ -145,6 +200,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
var archiveSemesterId = null;
|
||||
var archivePage = 1;
|
||||
var archiveTotalPages = 1;
|
||||
var associateSemesterId = null;
|
||||
|
||||
function fillSemesterDates(type) {
|
||||
var now = new Date();
|
||||
@@ -188,10 +244,14 @@ async function loadSemesters() {
|
||||
}
|
||||
|
||||
let actions = '';
|
||||
const startDate = sem.start_date || '';
|
||||
const endDate = sem.end_date || '';
|
||||
if (!sem.is_archived) {
|
||||
actions += `<button class="btn btn-sm" style="border:1px solid #667eea;color:#667eea;" onclick="showEditSemesterModal(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">编辑</button> `;
|
||||
if (!sem.is_active) {
|
||||
actions += `<button class="btn btn-sm btn-primary" onclick="activateSemester(${sem.semester_id})">激活</button> `;
|
||||
}
|
||||
actions += `<button class="btn btn-sm" style="border:1px solid #2ecc71;color:#2ecc71;" onclick="showAssociateConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">关联数据</button> `;
|
||||
actions += `<button class="btn btn-sm btn-warning" onclick="showArchiveConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">归档</button> `;
|
||||
}
|
||||
if (sem.is_archived) {
|
||||
@@ -260,8 +320,79 @@ async function activateSemester(semesterId) {
|
||||
}
|
||||
}
|
||||
|
||||
function showEditSemesterModal(id, name, startDate, endDate) {
|
||||
document.getElementById('editSemesterId').value = id;
|
||||
document.getElementById('editSemesterName').value = name;
|
||||
document.getElementById('editSemesterStartDate').value = startDate || '';
|
||||
document.getElementById('editSemesterEndDate').value = endDate || '';
|
||||
document.getElementById('editSemesterModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitEditSemester() {
|
||||
const id = document.getElementById('editSemesterId').value;
|
||||
const name = document.getElementById('editSemesterName').value.trim();
|
||||
const startDate = document.getElementById('editSemesterStartDate').value;
|
||||
const endDate = document.getElementById('editSemesterEndDate').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入学期名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { semester_name: name };
|
||||
if (startDate) data.start_date = startDate;
|
||||
if (endDate) data.end_date = endDate;
|
||||
|
||||
const res = await apiPut(`/api/semester/update/${id}`, data);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '更新成功');
|
||||
closeModal('editSemesterModal');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSemester() {
|
||||
const id = document.getElementById('editSemesterId').value;
|
||||
if (!confirm('确定要删除该学期吗?如果学期已有归档数据则无法删除。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiDelete(`/api/semester/delete/${id}`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '删除成功');
|
||||
closeModal('editSemesterModal');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '删除失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showAssociateConfirm(id, name, startDate, endDate) {
|
||||
associateSemesterId = id;
|
||||
const dateRange = startDate ? `${startDate} ~ ${endDate || '至今'}` : '未设置日期范围';
|
||||
document.getElementById('associateConfirmText').innerHTML =
|
||||
`即将关联 <strong>${dateRange}</strong> 内的所有未分配学期的操行分记录和考勤记录到学期 "<strong>${name}</strong>"。`;
|
||||
document.getElementById('associateConfirmModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function confirmAssociate() {
|
||||
if (!associateSemesterId) return;
|
||||
|
||||
const res = await apiPost(`/api/semester/${associateSemesterId}/associate`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '关联成功');
|
||||
closeModal('associateConfirmModal');
|
||||
associateSemesterId = null;
|
||||
} else {
|
||||
showToast(res?.message || '关联失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showArchiveConfirm(semesterId, semesterName) {
|
||||
archiveSemesterId = semesterId;
|
||||
document.getElementById('archiveResetScores').checked = false;
|
||||
document.getElementById('archiveConfirmText').innerHTML =
|
||||
`确定要归档学期 "<strong>${semesterName}</strong>" 吗?<br>归档后将保存所有学生的当前操行分快照,该学期数据将变为只读。`;
|
||||
document.getElementById('archiveConfirmModal').style.display = 'flex';
|
||||
@@ -270,7 +401,10 @@ function showArchiveConfirm(semesterId, semesterName) {
|
||||
async function confirmArchive() {
|
||||
if (!archiveSemesterId) return;
|
||||
|
||||
const res = await apiPost(`/api/semester/archive/${archiveSemesterId}`);
|
||||
const resetScores = document.getElementById('archiveResetScores').checked;
|
||||
const url = `/api/semester/archive/${archiveSemesterId}?reset_scores=${resetScores}`;
|
||||
|
||||
const res = await apiPost(url);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '归档成功');
|
||||
closeModal('archiveConfirmModal');
|
||||
|
||||
Reference in New Issue
Block a user