Добавил запрос на создание препода от кафедры

This commit is contained in:
dipatrik10
2026-06-02 00:01:14 +03:00
parent 4b05d4287c
commit 4adce0b89c
23 changed files with 1370 additions and 51 deletions

View File

@@ -2,17 +2,22 @@ package com.magistr.app.controller;
import com.magistr.app.config.auth.AuthContext;
import com.magistr.app.config.auth.RequireRoles;
import com.magistr.app.dto.CreateTeacherCreationRequest;
import com.magistr.app.dto.CreateSubjectRequest;
import com.magistr.app.dto.SubjectCommentDto;
import com.magistr.app.dto.TeacherCreationRequestResponse;
import com.magistr.app.dto.UserResponse;
import com.magistr.app.model.*;
import com.magistr.app.repository.DepartmentRepository;
import com.magistr.app.repository.SubjectCommentRepository;
import com.magistr.app.repository.SubjectRepository;
import com.magistr.app.repository.TeacherCreationRequestRepository;
import com.magistr.app.repository.TeacherDepartmentAssignmentRepository;
import com.magistr.app.repository.UserRepository;
import com.magistr.app.service.ScheduleQueryService;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
@@ -26,21 +31,29 @@ import java.util.Objects;
@RequireRoles({Role.ADMIN, Role.DEPARTMENT})
public class DepartmentWorkspaceController {
private static final Long NO_DEPARTMENT_ID = Long.MIN_VALUE;
private final SubjectRepository subjectRepository;
private final SubjectCommentRepository subjectCommentRepository;
private final UserRepository userRepository;
private final DepartmentRepository departmentRepository;
private final TeacherDepartmentAssignmentRepository teacherDepartmentAssignmentRepository;
private final TeacherCreationRequestRepository teacherCreationRequestRepository;
private final ScheduleQueryService scheduleQueryService;
public DepartmentWorkspaceController(SubjectRepository subjectRepository,
SubjectCommentRepository subjectCommentRepository,
UserRepository userRepository,
DepartmentRepository departmentRepository,
TeacherDepartmentAssignmentRepository teacherDepartmentAssignmentRepository,
TeacherCreationRequestRepository teacherCreationRequestRepository,
ScheduleQueryService scheduleQueryService) {
this.subjectRepository = subjectRepository;
this.subjectCommentRepository = subjectCommentRepository;
this.userRepository = userRepository;
this.departmentRepository = departmentRepository;
this.teacherDepartmentAssignmentRepository = teacherDepartmentAssignmentRepository;
this.teacherCreationRequestRepository = teacherCreationRequestRepository;
this.scheduleQueryService = scheduleQueryService;
}
@@ -127,6 +140,7 @@ public class DepartmentWorkspaceController {
teacherDepartmentAssignmentRepository.findDepartmentTeachersAtDate(effectiveDepartmentId, LocalDate.now())
.stream()
.map(TeacherDepartmentAssignment::getTeacher)
.filter(Objects::nonNull)
.filter(User::isActiveRecord)
.forEach(teacher -> teachersById.put(teacher.getId(), teacher));
userRepository.findByRoleAndDepartmentIdAndStatusNot(Role.TEACHER, effectiveDepartmentId, LifecycleEntity.STATUS_ARCHIVED)
@@ -136,6 +150,90 @@ public class DepartmentWorkspaceController {
.toList();
}
@PostMapping("/teachers/{teacherId}/assignments")
@Transactional
public ResponseEntity<?> addTeacherToDepartment(@PathVariable Long teacherId,
@RequestBody(required = false) Map<String, Object> request) {
Long requestedDepartmentId = asLong(request == null ? null : request.get("departmentId"));
Long departmentId = effectiveDepartmentId(requestedDepartmentId);
if (departmentId == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Кафедра обязательна"));
}
Department department = departmentRepository.findById(departmentId).orElse(null);
if (department == null || department.isArchivedRecord()) {
return ResponseEntity.badRequest().body(Map.of("message", "Активная кафедра не найдена"));
}
User teacher = userRepository.findById(teacherId).orElse(null);
if (teacher == null || teacher.getRole() != Role.TEACHER || teacher.isArchivedRecord()) {
return ResponseEntity.badRequest().body(Map.of("message", "Активный преподаватель не найден"));
}
LocalDate today = LocalDate.now();
if (teacherDepartmentAssignmentRepository.existsActiveAssignment(teacherId, departmentId, today)) {
return ResponseEntity.badRequest().body(Map.of("message", "Преподаватель уже привязан к этой кафедре"));
}
TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();
assignment.setTeacher(teacher);
assignment.setDepartment(department);
assignment.setValidFrom(today);
assignment.setPrimaryAssignment(false);
assignment.setComment(trimToNull(request == null ? null : request.get("comment")));
assignment.setCreatedBy(AuthContext.currentUserId());
teacherDepartmentAssignmentRepository.save(assignment);
return ResponseEntity.ok(toAssignmentMap(assignment));
}
@GetMapping("/teacher-requests")
public List<TeacherCreationRequestResponse> getTeacherRequests(@RequestParam(required = false) Long departmentId) {
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
if (effectiveDepartmentId == null) {
return teacherCreationRequestRepository.findAllByOrderByCreatedAtDesc().stream()
.map(this::toTeacherCreationRequestResponse)
.toList();
}
return teacherCreationRequestRepository.findByDepartment_IdOrderByCreatedAtDesc(effectiveDepartmentId).stream()
.map(this::toTeacherCreationRequestResponse)
.toList();
}
@PostMapping("/teacher-requests")
public ResponseEntity<?> createTeacherRequest(@RequestBody CreateTeacherCreationRequest request) {
if (request == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Данные заявки обязательны"));
}
Long departmentId = effectiveDepartmentId(request == null ? null : request.getDepartmentId());
if (departmentId == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Кафедра обязательна"));
}
Department department = departmentRepository.findById(departmentId).orElse(null);
if (department == null || department.isArchivedRecord()) {
return ResponseEntity.badRequest().body(Map.of("message", "Активная кафедра не найдена"));
}
String username = trimToNull(request.getUsername());
String fullName = trimToNull(request.getFullName());
String jobTitle = trimToNull(request.getJobTitle());
if (username == null || fullName == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Логин и ФИО преподавателя обязательны"));
}
if (username.length() > 50 || fullName.length() > 255 || (jobTitle != null && jobTitle.length() > 255)) {
return ResponseEntity.badRequest().body(Map.of("message", "Проверьте длину полей заявки"));
}
if (userRepository.findByUsername(username).isPresent()
|| teacherCreationRequestRepository.existsByUsernameAndStatus(username, TeacherCreationRequestStatus.PENDING)) {
return ResponseEntity.badRequest().body(Map.of("message", "Логин уже занят или ожидает одобрения"));
}
TeacherCreationRequest teacherRequest = new TeacherCreationRequest();
teacherRequest.setDepartment(department);
teacherRequest.setUsername(username);
teacherRequest.setFullName(fullName);
teacherRequest.setJobTitle(jobTitle == null ? "Не указано" : jobTitle);
teacherRequest.setComment(trimToNull(request.getComment()));
teacherRequest.setRequestedBy(AuthContext.currentUserId());
return ResponseEntity.ok(toTeacherCreationRequestResponse(teacherCreationRequestRepository.save(teacherRequest)));
}
@GetMapping("/schedule")
public ResponseEntity<?> getSchedule(
@RequestParam(required = false) Long departmentId,
@@ -172,10 +270,64 @@ public class DepartmentWorkspaceController {
return response;
}
private TeacherCreationRequestResponse toTeacherCreationRequestResponse(TeacherCreationRequest request) {
Department department = request.getDepartment();
return new TeacherCreationRequestResponse(
request.getId(),
department == null ? null : department.getId(),
department == null ? null : department.getDepartmentName(),
request.getUsername(),
request.getFullName(),
request.getJobTitle(),
request.getComment(),
request.getStatus().name(),
request.getRequestedBy(),
request.getCreatedAt(),
request.getReviewedBy(),
request.getReviewedAt(),
request.getReviewComment(),
request.getCreatedTeacherId()
);
}
private Map<String, Object> toAssignmentMap(TeacherDepartmentAssignment assignment) {
Map<String, Object> response = new LinkedHashMap<>();
response.put("id", assignment.getId());
response.put("teacherId", assignment.getTeacher().getId());
response.put("teacherName", assignment.getTeacher().getFullName());
response.put("departmentId", assignment.getDepartment().getId());
response.put("departmentName", assignment.getDepartment().getDepartmentName());
response.put("validFrom", assignment.getValidFrom());
response.put("primaryAssignment", assignment.getPrimaryAssignment());
return response;
}
private Long asLong(Object value) {
if (value instanceof Number number) {
return number.longValue();
}
if (value instanceof String text && !text.isBlank()) {
try {
return Long.parseLong(text);
} catch (NumberFormatException ignored) {
return null;
}
}
return null;
}
private String trimToNull(Object value) {
if (value == null) {
return null;
}
String text = value.toString().trim();
return text.isEmpty() ? null : text;
}
private Long effectiveDepartmentId(Long requestedDepartmentId) {
var user = AuthContext.getCurrentUser();
if (user != null && user.role() == Role.DEPARTMENT) {
return user.departmentId();
return user.departmentId() == null ? NO_DEPARTMENT_ID : user.departmentId();
}
return requestedDepartmentId == null && user != null ? user.departmentId() : requestedDepartmentId;
}

View File

@@ -0,0 +1,190 @@
package com.magistr.app.controller;
import com.magistr.app.config.auth.AuthContext;
import com.magistr.app.config.auth.RequireRoles;
import com.magistr.app.dto.ReviewTeacherCreationRequest;
import com.magistr.app.dto.TeacherCreationRequestResponse;
import com.magistr.app.dto.UserResponse;
import com.magistr.app.model.*;
import com.magistr.app.repository.DepartmentRepository;
import com.magistr.app.repository.TeacherCreationRequestRepository;
import com.magistr.app.repository.TeacherDepartmentAssignmentRepository;
import com.magistr.app.repository.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/teacher-requests")
@RequireRoles({Role.ADMIN})
public class TeacherCreationRequestController {
private final TeacherCreationRequestRepository teacherCreationRequestRepository;
private final UserRepository userRepository;
private final DepartmentRepository departmentRepository;
private final TeacherDepartmentAssignmentRepository teacherDepartmentAssignmentRepository;
private final BCryptPasswordEncoder passwordEncoder;
public TeacherCreationRequestController(TeacherCreationRequestRepository teacherCreationRequestRepository,
UserRepository userRepository,
DepartmentRepository departmentRepository,
TeacherDepartmentAssignmentRepository teacherDepartmentAssignmentRepository,
BCryptPasswordEncoder passwordEncoder) {
this.teacherCreationRequestRepository = teacherCreationRequestRepository;
this.userRepository = userRepository;
this.departmentRepository = departmentRepository;
this.teacherDepartmentAssignmentRepository = teacherDepartmentAssignmentRepository;
this.passwordEncoder = passwordEncoder;
}
@GetMapping
public List<TeacherCreationRequestResponse> getRequests(@RequestParam(required = false) TeacherCreationRequestStatus status) {
return (status == null
? teacherCreationRequestRepository.findAllByOrderByCreatedAtDesc()
: teacherCreationRequestRepository.findByStatusOrderByCreatedAtDesc(status))
.stream()
.map(this::toResponse)
.toList();
}
@PostMapping("/{id}/approve")
@Transactional
public ResponseEntity<?> approve(@PathVariable Long id, @RequestBody ReviewTeacherCreationRequest review) {
TeacherCreationRequest request = teacherCreationRequestRepository.findById(id).orElse(null);
if (request == null) {
return ResponseEntity.notFound().build();
}
if (request.getStatus() != TeacherCreationRequestStatus.PENDING) {
return ResponseEntity.badRequest().body(Map.of("message", "Заявка уже обработана"));
}
if (review == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Данные одобрения обязательны"));
}
String username = firstNotBlank(review.getUsername(), request.getUsername());
String fullName = firstNotBlank(review.getFullName(), request.getFullName());
String jobTitle = firstNotBlank(review.getJobTitle(), request.getJobTitle(), "Не указано");
if (username.length() > 50 || fullName.length() > 255 || jobTitle.length() > 255) {
return ResponseEntity.badRequest().body(Map.of("message", "Проверьте длину полей преподавателя"));
}
if (review.getPassword() == null || review.getPassword().length() < 8) {
return ResponseEntity.badRequest().body(Map.of("message", "Пароль преподавателя должен быть не короче 8 символов"));
}
if (userRepository.findByUsername(username).isPresent()) {
return ResponseEntity.badRequest().body(Map.of("message", "Пользователь с таким логином уже существует"));
}
Long departmentId = review.getDepartmentId() == null ? request.getDepartment().getId() : review.getDepartmentId();
Department department = departmentRepository.findById(departmentId).orElse(null);
if (department == null || department.isArchivedRecord()) {
return ResponseEntity.badRequest().body(Map.of("message", "Активная кафедра не найдена"));
}
User teacher = new User();
teacher.setUsername(username);
teacher.setPassword(passwordEncoder.encode(review.getPassword()));
teacher.setRole(Role.TEACHER);
teacher.setFullName(fullName);
teacher.setJobTitle(jobTitle);
teacher.setDepartmentId(department.getId());
userRepository.save(teacher);
TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();
assignment.setTeacher(teacher);
assignment.setDepartment(department);
assignment.setValidFrom(LocalDate.now());
assignment.setPrimaryAssignment(true);
assignment.setComment("Создан по заявке кафедры #" + request.getId());
assignment.setCreatedBy(AuthContext.currentUserId());
teacherDepartmentAssignmentRepository.save(assignment);
request.setUsername(username);
request.setFullName(fullName);
request.setJobTitle(jobTitle);
request.setDepartment(department);
request.setStatus(TeacherCreationRequestStatus.APPROVED);
request.setReviewedBy(AuthContext.currentUserId());
request.setReviewedAt(LocalDateTime.now());
request.setReviewComment(trimToNull(review.getReviewComment()));
request.setCreatedTeacherId(teacher.getId());
teacherCreationRequestRepository.save(request);
return ResponseEntity.ok(Map.of(
"request", toResponse(request),
"teacher", toUserResponse(teacher, department)
));
}
@PostMapping("/{id}/reject")
public ResponseEntity<?> reject(@PathVariable Long id, @RequestBody(required = false) ReviewTeacherCreationRequest review) {
TeacherCreationRequest request = teacherCreationRequestRepository.findById(id).orElse(null);
if (request == null) {
return ResponseEntity.notFound().build();
}
if (request.getStatus() != TeacherCreationRequestStatus.PENDING) {
return ResponseEntity.badRequest().body(Map.of("message", "Заявка уже обработана"));
}
request.setStatus(TeacherCreationRequestStatus.REJECTED);
request.setReviewedBy(AuthContext.currentUserId());
request.setReviewedAt(LocalDateTime.now());
request.setReviewComment(trimToNull(review == null ? null : review.getReviewComment()));
return ResponseEntity.ok(toResponse(teacherCreationRequestRepository.save(request)));
}
private TeacherCreationRequestResponse toResponse(TeacherCreationRequest request) {
Department department = request.getDepartment();
return new TeacherCreationRequestResponse(
request.getId(),
department == null ? null : department.getId(),
department == null ? null : department.getDepartmentName(),
request.getUsername(),
request.getFullName(),
request.getJobTitle(),
request.getComment(),
request.getStatus().name(),
request.getRequestedBy(),
request.getCreatedAt(),
request.getReviewedBy(),
request.getReviewedAt(),
request.getReviewComment(),
request.getCreatedTeacherId()
);
}
private UserResponse toUserResponse(User user, Department department) {
UserResponse response = new UserResponse(
user.getId(),
user.getUsername(),
user.getRole().name(),
user.getFullName(),
user.getJobTitle(),
department.getDepartmentName()
);
response.setDepartmentId(department.getId());
response.setStatus(user.getStatus());
return response;
}
private String firstNotBlank(String... values) {
for (String value : values) {
String normalized = trimToNull(value);
if (normalized != null) {
return normalized;
}
}
return "";
}
private String trimToNull(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value.trim();
}
}

View File

@@ -24,8 +24,10 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@RestController
@RequestMapping("/api/users")
@@ -75,12 +77,12 @@ public class UserController {
@GetMapping("/teachers/{departmentId}")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
public ResponseEntity<?> getTeachersByDepartmentId(@PathVariable Long departmentId){
if (!canAccessDepartment(departmentId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("message", "Недостаточно прав для просмотра преподавателей этой кафедры"));
}
logger.info("Получен запрос на получение преподавателей для кафедры с ID - {}", departmentId);
List<User> users = userRepository.findByRoleAndDepartmentIdAndStatusNot(
Role.TEACHER,
departmentId,
LifecycleEntity.STATUS_ARCHIVED
);
List<User> users = teachersForDepartment(departmentId, LocalDate.now());
if (users.isEmpty()) {
logger.info("Преподаватели для кафедры с ID - {} не найдены", departmentId);
@@ -96,6 +98,11 @@ public class UserController {
@PostMapping
public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request) {
if (request == null) {
String errorMessage = "Данные пользователя обязательны";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
logger.info("Получен запрос на создание нового пользователя: username = {}, fullName = {}, jobTitle = {}, departmentId = {}", request.getUsername(), request.getFullName(), request.getJobTitle(), request.getDepartmentId());
if (request.getUsername() == null || request.getUsername().isBlank()) {
@@ -103,8 +110,8 @@ public class UserController {
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
if (request.getPassword() == null || request.getPassword().length() < 4) {
String errorMessage = "Пароль минимум 4 символа";
if (request.getPassword() == null || request.getPassword().length() < 8) {
String errorMessage = "Пароль минимум 8 символов";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
@@ -127,6 +134,12 @@ public class UserController {
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
Department requestedDepartment = departmentRepository.findById(request.getDepartmentId()).orElse(null);
if (requestedDepartment == null || requestedDepartment.isArchivedRecord()) {
String errorMessage = "Активная кафедра не найдена";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
Role role;
try {
@@ -145,17 +158,14 @@ public class UserController {
user.setDepartmentId(request.getDepartmentId());
userRepository.save(user);
if (role == Role.TEACHER) {
Department department = departmentRepository.findById(user.getDepartmentId()).orElse(null);
if (department != null) {
TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();
assignment.setTeacher(user);
assignment.setDepartment(department);
assignment.setValidFrom(LocalDate.now());
assignment.setPrimaryAssignment(true);
assignment.setComment("Начальная кафедра при создании пользователя");
assignment.setCreatedBy(AuthContext.currentUserId());
teacherDepartmentAssignmentRepository.save(assignment);
}
TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();
assignment.setTeacher(user);
assignment.setDepartment(requestedDepartment);
assignment.setValidFrom(LocalDate.now());
assignment.setPrimaryAssignment(true);
assignment.setComment("Начальная кафедра при создании пользователя");
assignment.setCreatedBy(AuthContext.currentUserId());
teacherDepartmentAssignmentRepository.save(assignment);
}
logger.info("Пользователь успешно создан с ID: {}", user.getId());
@@ -193,6 +203,13 @@ public class UserController {
if (!userRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
var currentUser = AuthContext.getCurrentUser();
if (currentUser != null
&& currentUser.role() == Role.DEPARTMENT
&& !teacherDepartmentAssignmentRepository.existsActiveAssignment(id, currentUser.departmentId(), LocalDate.now())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("message", "Недостаточно прав для просмотра истории этого преподавателя"));
}
return ResponseEntity.ok(teacherDepartmentAssignmentRepository.findHistoryByTeacherId(id).stream()
.map(this::toAssignmentDto)
.toList());
@@ -247,12 +264,12 @@ public class UserController {
public ResponseEntity<?> getTeachersByDepartmentAtDate(@PathVariable Long departmentId,
@RequestParam(required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
if (!canAccessDepartment(departmentId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("message", "Недостаточно прав для просмотра преподавателей этой кафедры"));
}
LocalDate effectiveDate = date == null ? LocalDate.now() : date;
return ResponseEntity.ok(teacherDepartmentAssignmentRepository
.findDepartmentTeachersAtDate(departmentId, effectiveDate)
.stream()
.map(TeacherDepartmentAssignment::getTeacher)
.filter(User::isActiveRecord)
return ResponseEntity.ok(teachersForDepartment(departmentId, effectiveDate).stream()
.map(this::toUserResponse)
.toList());
}
@@ -289,4 +306,28 @@ public class UserController {
assignment.getComment()
);
}
private List<User> teachersForDepartment(Long departmentId, LocalDate date) {
Map<Long, User> teachersById = new LinkedHashMap<>();
List<TeacherDepartmentAssignment> assignments = teacherDepartmentAssignmentRepository.findDepartmentTeachersAtDate(departmentId, date);
if (assignments != null) {
assignments.stream()
.map(TeacherDepartmentAssignment::getTeacher)
.filter(Objects::nonNull)
.filter(User::isActiveRecord)
.forEach(teacher -> teachersById.put(teacher.getId(), teacher));
}
List<User> directTeachers = userRepository.findByRoleAndDepartmentIdAndStatusNot(Role.TEACHER, departmentId, LifecycleEntity.STATUS_ARCHIVED);
if (directTeachers != null) {
directTeachers.forEach(teacher -> teachersById.putIfAbsent(teacher.getId(), teacher));
}
return List.copyOf(teachersById.values());
}
private boolean canAccessDepartment(Long departmentId) {
var currentUser = AuthContext.getCurrentUser();
return currentUser == null
|| currentUser.role() != Role.DEPARTMENT
|| (departmentId != null && departmentId.equals(currentUser.departmentId()));
}
}

View File

@@ -0,0 +1,50 @@
package com.magistr.app.dto;
public class CreateTeacherCreationRequest {
private String username;
private String fullName;
private String jobTitle;
private String comment;
private Long departmentId;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getJobTitle() {
return jobTitle;
}
public void setJobTitle(String jobTitle) {
this.jobTitle = jobTitle;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public Long getDepartmentId() {
return departmentId;
}
public void setDepartmentId(Long departmentId) {
this.departmentId = departmentId;
}
}

View File

@@ -0,0 +1,59 @@
package com.magistr.app.dto;
public class ReviewTeacherCreationRequest {
private String username;
private String fullName;
private String jobTitle;
private Long departmentId;
private String password;
private String reviewComment;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getJobTitle() {
return jobTitle;
}
public void setJobTitle(String jobTitle) {
this.jobTitle = jobTitle;
}
public Long getDepartmentId() {
return departmentId;
}
public void setDepartmentId(Long departmentId) {
this.departmentId = departmentId;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getReviewComment() {
return reviewComment;
}
public void setReviewComment(String reviewComment) {
this.reviewComment = reviewComment;
}
}

View File

@@ -0,0 +1,21 @@
package com.magistr.app.dto;
import java.time.LocalDateTime;
public record TeacherCreationRequestResponse(
Long id,
Long departmentId,
String departmentName,
String username,
String fullName,
String jobTitle,
String comment,
String status,
Long requestedBy,
LocalDateTime createdAt,
Long reviewedBy,
LocalDateTime reviewedAt,
String reviewComment,
Long createdTeacherId
) {
}

View File

@@ -0,0 +1,168 @@
package com.magistr.app.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "teacher_creation_requests")
public class TeacherCreationRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "department_id", nullable = false)
private Department department;
@Column(nullable = false, length = 50)
private String username;
@Column(name = "full_name", nullable = false, length = 255)
private String fullName;
@Column(name = "job_title", nullable = false, length = 255)
private String jobTitle;
@Column(columnDefinition = "TEXT")
private String comment;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private TeacherCreationRequestStatus status = TeacherCreationRequestStatus.PENDING;
@Column(name = "requested_by")
private Long requestedBy;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt = LocalDateTime.now();
@Column(name = "reviewed_by")
private Long reviewedBy;
@Column(name = "reviewed_at")
private LocalDateTime reviewedAt;
@Column(name = "review_comment", columnDefinition = "TEXT")
private String reviewComment;
@Column(name = "created_teacher_id")
private Long createdTeacherId;
@PreUpdate
public void touch() {
updatedAt = LocalDateTime.now();
}
public Long getId() {
return id;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getJobTitle() {
return jobTitle;
}
public void setJobTitle(String jobTitle) {
this.jobTitle = jobTitle;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public TeacherCreationRequestStatus getStatus() {
return status;
}
public void setStatus(TeacherCreationRequestStatus status) {
this.status = status == null ? TeacherCreationRequestStatus.PENDING : status;
}
public Long getRequestedBy() {
return requestedBy;
}
public void setRequestedBy(Long requestedBy) {
this.requestedBy = requestedBy;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public Long getReviewedBy() {
return reviewedBy;
}
public void setReviewedBy(Long reviewedBy) {
this.reviewedBy = reviewedBy;
}
public LocalDateTime getReviewedAt() {
return reviewedAt;
}
public void setReviewedAt(LocalDateTime reviewedAt) {
this.reviewedAt = reviewedAt;
}
public String getReviewComment() {
return reviewComment;
}
public void setReviewComment(String reviewComment) {
this.reviewComment = reviewComment;
}
public Long getCreatedTeacherId() {
return createdTeacherId;
}
public void setCreatedTeacherId(Long createdTeacherId) {
this.createdTeacherId = createdTeacherId;
}
}

View File

@@ -0,0 +1,7 @@
package com.magistr.app.model;
public enum TeacherCreationRequestStatus {
PENDING,
APPROVED,
REJECTED
}

View File

@@ -0,0 +1,18 @@
package com.magistr.app.repository;
import com.magistr.app.model.TeacherCreationRequest;
import com.magistr.app.model.TeacherCreationRequestStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface TeacherCreationRequestRepository extends JpaRepository<TeacherCreationRequest, Long> {
List<TeacherCreationRequest> findByDepartment_IdOrderByCreatedAtDesc(Long departmentId);
List<TeacherCreationRequest> findByStatusOrderByCreatedAtDesc(TeacherCreationRequestStatus status);
List<TeacherCreationRequest> findAllByOrderByCreatedAtDesc();
boolean existsByUsernameAndStatus(String username, TeacherCreationRequestStatus status);
}

View File

@@ -38,7 +38,6 @@ public interface TeacherDepartmentAssignmentRepository extends JpaRepository<Tea
join fetch a.teacher
join fetch a.department
where a.department.id = :departmentId
and a.primaryAssignment = true
and a.validFrom <= :date
and (a.validTo is null or a.validTo >= :date)
order by a.teacher.fullName
@@ -47,4 +46,18 @@ public interface TeacherDepartmentAssignmentRepository extends JpaRepository<Tea
@Param("departmentId") Long departmentId,
@Param("date") LocalDate date
);
@Query("""
select count(a) > 0
from TeacherDepartmentAssignment a
where a.teacher.id = :teacherId
and a.department.id = :departmentId
and a.validFrom <= :date
and (a.validTo is null or a.validTo >= :date)
""")
boolean existsActiveAssignment(
@Param("teacherId") Long teacherId,
@Param("departmentId") Long departmentId,
@Param("date") LocalDate date
);
}

View File

@@ -133,6 +133,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_teacher_department_open_primary
ON teacher_department_assignments(teacher_id)
WHERE is_primary = TRUE AND valid_to IS NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uq_teacher_department_open_pair
ON teacher_department_assignments(teacher_id, department_id)
WHERE valid_to IS NULL;
INSERT INTO teacher_department_assignments (teacher_id, department_id, valid_from, is_primary, comment)
SELECT u.id, u.department_id, COALESCE(u.created_at::DATE, CURRENT_DATE), TRUE, 'Начальная кафедра из users.department_id'
FROM users u
@@ -143,6 +147,34 @@ WHERE u.role = 'TEACHER'
WHERE existing.teacher_id = u.id
);
CREATE TABLE IF NOT EXISTS teacher_creation_requests (
id BIGSERIAL PRIMARY KEY,
department_id BIGINT NOT NULL REFERENCES departments(id),
username VARCHAR(50) NOT NULL,
full_name VARCHAR(255) NOT NULL,
job_title VARCHAR(255) NOT NULL,
comment TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
requested_by BIGINT REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
reviewed_by BIGINT REFERENCES users(id),
reviewed_at TIMESTAMP,
review_comment TEXT,
created_teacher_id BIGINT REFERENCES users(id),
CONSTRAINT chk_teacher_creation_request_status CHECK (status IN ('PENDING', 'APPROVED', 'REJECTED'))
);
CREATE INDEX IF NOT EXISTS idx_teacher_creation_requests_department
ON teacher_creation_requests(department_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_teacher_creation_requests_status
ON teacher_creation_requests(status, created_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS uq_teacher_creation_requests_pending_username
ON teacher_creation_requests(username)
WHERE status = 'PENDING';
-- ==========================================
-- Образовательные формы
-- ==========================================
@@ -938,6 +970,7 @@ COMMENT ON TABLE schedule_rule_groups IS 'Привязка правил расп
COMMENT ON TABLE schedule_rule_slots IS 'Слоты проведения занятий внутри правила расписания';
COMMENT ON TABLE schedule_rule_slot_subgroups IS 'Подгруппы лабораторных слотов правила расписания';
COMMENT ON TABLE teacher_department_assignments IS 'История принадлежности преподавателей к кафедрам';
COMMENT ON TABLE teacher_creation_requests IS 'Заявки кафедр на создание преподавателей';
COMMENT ON TABLE subject_comments IS 'Комментарии кафедр и администраторов к дисциплинам';
COMMENT ON TABLE schedule_overrides IS 'Точечные изменения конкретных сгенерированных пар';
COMMENT ON COLUMN schedule_rules.lecture_academic_hours IS 'Лимит академических часов лекций';

View File

@@ -11,18 +11,23 @@ import com.magistr.app.model.TeacherDepartmentAssignment;
import com.magistr.app.model.User;
import com.magistr.app.repository.SubjectCommentRepository;
import com.magistr.app.repository.SubjectRepository;
import com.magistr.app.repository.DepartmentRepository;
import com.magistr.app.repository.TeacherCreationRequestRepository;
import com.magistr.app.repository.TeacherDepartmentAssignmentRepository;
import com.magistr.app.repository.UserRepository;
import com.magistr.app.service.ScheduleQueryService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class DepartmentWorkspaceControllerTest {
@@ -43,7 +48,9 @@ class DepartmentWorkspaceControllerTest {
subjectRepository,
mock(SubjectCommentRepository.class),
mock(UserRepository.class),
mock(DepartmentRepository.class),
mock(TeacherDepartmentAssignmentRepository.class),
mock(TeacherCreationRequestRepository.class),
mock(ScheduleQueryService.class)
);
AuthContext.setCurrentUser(new AuthenticatedUser(1L, "кафедра", Role.DEPARTMENT, 2L));
@@ -67,7 +74,9 @@ class DepartmentWorkspaceControllerTest {
mock(SubjectRepository.class),
mock(SubjectCommentRepository.class),
userRepository,
mock(DepartmentRepository.class),
assignmentRepository,
mock(TeacherCreationRequestRepository.class),
mock(ScheduleQueryService.class)
);
@@ -77,6 +86,39 @@ class DepartmentWorkspaceControllerTest {
.containsExactly("Петров Препод Петрович", "Препод Тест Тестович");
}
@Test
void departmentCanAddExistingTeacherAsSecondaryAssignment() {
UserRepository userRepository = mock(UserRepository.class);
DepartmentRepository departmentRepository = mock(DepartmentRepository.class);
TeacherDepartmentAssignmentRepository assignmentRepository = mock(TeacherDepartmentAssignmentRepository.class);
User teacher = user(20L, "teacher", "Иванов Иван Иванович", 2L);
Department department = new Department(1L, "Кафедра ИБ", 1L);
when(userRepository.findById(20L)).thenReturn(Optional.of(teacher));
when(departmentRepository.findById(1L)).thenReturn(Optional.of(department));
when(assignmentRepository.existsActiveAssignment(20L, 1L, LocalDate.now())).thenReturn(false);
DepartmentWorkspaceController controller = new DepartmentWorkspaceController(
mock(SubjectRepository.class),
mock(SubjectCommentRepository.class),
userRepository,
departmentRepository,
assignmentRepository,
mock(TeacherCreationRequestRepository.class),
mock(ScheduleQueryService.class)
);
AuthContext.setCurrentUser(new AuthenticatedUser(3L, "department", Role.DEPARTMENT, 1L));
var response = controller.addTeacherToDepartment(20L, Map.of("comment", "Совместительство"));
assertThat(response.getStatusCode().value()).isEqualTo(200);
ArgumentCaptor<TeacherDepartmentAssignment> captor = ArgumentCaptor.forClass(TeacherDepartmentAssignment.class);
verify(assignmentRepository).save(captor.capture());
TeacherDepartmentAssignment saved = captor.getValue();
assertThat(saved.getTeacher()).isEqualTo(teacher);
assertThat(saved.getDepartment()).isEqualTo(department);
assertThat(saved.getPrimaryAssignment()).isFalse();
assertThat(saved.getCreatedBy()).isEqualTo(3L);
}
private TeacherDepartmentAssignment assignment(User teacher, Long departmentId) {
Department department = new Department(departmentId, "Кафедра", departmentId);
TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();

View File

@@ -23,13 +23,16 @@ class UserControllerTest {
void teachersUseUnifiedUserResponseMapping() {
UserRepository userRepository = mock(UserRepository.class);
DepartmentRepository departmentRepository = mock(DepartmentRepository.class);
TeacherDepartmentAssignmentRepository assignmentRepository = mock(TeacherDepartmentAssignmentRepository.class);
UserController controller = new UserController(
userRepository,
null,
departmentRepository,
mock(TeacherDepartmentAssignmentRepository.class)
assignmentRepository
);
User teacher = user(10L, "teacher", Role.TEACHER, 2L);
when(assignmentRepository.findDepartmentTeachersAtDate(org.mockito.ArgumentMatchers.eq(2L), org.mockito.ArgumentMatchers.any()))
.thenReturn(List.of());
when(userRepository.findByRoleAndDepartmentIdAndStatusNot(Role.TEACHER, 2L, LifecycleEntity.STATUS_ARCHIVED))
.thenReturn(List.of(teacher));
when(departmentRepository.findById(2L))