Много разных изменений

This commit is contained in:
Zuev
2026-08-20 00:55:27 +03:00
parent 3599d850f6
commit a1c4ee4298
78 changed files with 4698 additions and 428 deletions

View File

@@ -8,6 +8,8 @@ import com.magistr.app.service.AcademicCalendarGridService;
import com.magistr.app.service.AcademicStructureService;
import com.magistr.app.service.ScheduleGeneratorService;
import org.springframework.http.ResponseEntity;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
@@ -18,6 +20,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.ArrayList;
import java.util.Locale;
import java.util.stream.Collectors;
@RestController
@@ -58,6 +62,32 @@ public class AcademicCalendarController {
.toList();
}
@GetMapping("/options")
public List<AcademicCalendarDto> searchCalendarOptions(
@RequestParam(defaultValue = "") String query,
@RequestParam(required = false) Long academicYearId,
@RequestParam(required = false) Long specialtyId,
@RequestParam(required = false) Long profileId,
@RequestParam(required = false) Long studyFormId,
@RequestParam(required = false) Long selectedId,
@RequestParam(defaultValue = "20") int limit) {
String normalizedQuery = query == null ? "" : query.trim().toLowerCase(Locale.ROOT);
List<AcademicCalendarDto> options = new ArrayList<>(calendarRepository.searchOptions(
normalizedQuery,
academicYearId,
specialtyId,
profileId,
studyFormId,
PageRequest.of(0, Math.max(1, Math.min(limit, 50)), Sort.by("title").ascending())
).stream()
.map(this::toCalendarDto)
.toList());
if (selectedId != null && options.stream().noneMatch(item -> selectedId.equals(item.id()))) {
calendarRepository.findByIdWithDetails(selectedId).map(this::toCalendarDto).ifPresent(options::add);
}
return options;
}
@GetMapping("/{id}")
public ResponseEntity<?> getCalendar(@PathVariable Long id) {
return calendarRepository.findByIdWithDetails(id)

View File

@@ -10,11 +10,15 @@ import com.magistr.app.model.Role;
import com.magistr.app.repository.ClassroomRepository;
import com.magistr.app.repository.EquipmentRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ArrayList;
import java.util.Locale;
@RestController
@RequestMapping("/api/classrooms")
@@ -40,6 +44,26 @@ public class ClassroomController {
.toList();
}
@GetMapping("/options")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
public List<ClassroomResponse> searchClassroomOptions(
@RequestParam(defaultValue = "") String query,
@RequestParam(required = false) Long selectedId,
@RequestParam(defaultValue = "20") int limit) {
String normalizedQuery = query == null ? "" : query.trim().toLowerCase(Locale.ROOT);
List<ClassroomResponse> options = new ArrayList<>(classroomRepository.searchActiveOptions(
normalizedQuery,
LifecycleEntity.STATUS_ARCHIVED,
PageRequest.of(0, Math.max(1, Math.min(limit, 50)), Sort.by("name").ascending())
).stream()
.map(this::mapToResponse)
.toList());
if (selectedId != null && options.stream().noneMatch(item -> selectedId.equals(item.getId()))) {
classroomRepository.findById(selectedId).map(this::mapToResponse).ifPresent(options::add);
}
return options;
}
@PostMapping
public ResponseEntity<?> createClassroom(@RequestBody ClassroomRequest request) {
if (request.getName() == null || request.getName().isBlank()) {

View File

@@ -1,10 +1,13 @@
package com.magistr.app.controller;
import com.magistr.app.config.auth.AuthContext;
import com.magistr.app.config.auth.RequireRoles;
import com.magistr.app.dto.AcademicCalendarSubjectDto;
import com.magistr.app.dto.CreateGroupRequest;
import com.magistr.app.dto.GroupCalendarAssignmentDto;
import com.magistr.app.dto.GroupResponse;
import com.magistr.app.dto.PageResponse;
import com.magistr.app.model.LifecycleEntity;
import com.magistr.app.model.AcademicCalendarSubject;
import com.magistr.app.model.Role;
import com.magistr.app.model.Speciality;
@@ -24,12 +27,19 @@ import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -98,6 +108,118 @@ public class GroupController {
}
}
@GetMapping("/page")
public PageResponse<GroupResponse> getGroupsPage(
@RequestParam(defaultValue = "") String query,
@RequestParam(required = false) List<Long> educationFormId,
@RequestParam(required = false) Long departmentId,
@RequestParam(required = false) Integer yearStartStudy,
@RequestParam(defaultValue = "true") boolean includeArchived,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "25") int size,
@RequestParam(defaultValue = "name") String sort,
@RequestParam(defaultValue = "asc") String direction) {
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
Specification<StudentGroup> specification = groupSpecification(
query, educationFormId, effectiveDepartmentId, yearStartStudy, includeArchived
);
Page<StudentGroup> groups = groupRepository.findAll(
specification,
PageRequest.of(normalizePage(page), normalizeSize(size), groupSort(sort, direction))
);
return PageResponse.from(groups.map(this::mapToResponse));
}
@GetMapping("/options")
public List<GroupResponse> searchGroupOptions(
@RequestParam(defaultValue = "") String query,
@RequestParam(required = false) Long departmentId,
@RequestParam(required = false) Long selectedId,
@RequestParam(defaultValue = "20") int limit) {
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
List<GroupResponse> options = new ArrayList<>(groupRepository.findAll(
groupSpecification(query, null, effectiveDepartmentId, null, false),
PageRequest.of(0, Math.min(normalizeSize(limit), 50), Sort.by("name").ascending())
).stream()
.filter(group -> groupLifecycleService.isAvailableForSelection(group, businessTime.today()))
.map(this::mapToResponse)
.toList());
if (selectedId != null && options.stream().noneMatch(item -> selectedId.equals(item.getId()))) {
groupRepository.findById(selectedId)
.filter(group -> effectiveDepartmentId == null
|| Objects.equals(group.getDepartmentId(), effectiveDepartmentId))
.filter(group -> groupLifecycleService.isAvailableForSelection(group, businessTime.today()))
.map(this::mapToResponse)
.ifPresent(options::add);
}
return options;
}
private Long effectiveDepartmentId(Long requestedDepartmentId) {
var currentUser = AuthContext.getCurrentUser();
return currentUser != null && currentUser.role() == Role.DEPARTMENT
? currentUser.departmentId()
: requestedDepartmentId;
}
private Specification<StudentGroup> groupSpecification(String query,
List<Long> educationFormIds,
Long departmentId,
Integer yearStartStudy,
boolean includeArchived) {
return (root, criteriaQuery, builder) -> {
List<jakarta.persistence.criteria.Predicate> predicates = new ArrayList<>();
if (!includeArchived) {
predicates.add(builder.notEqual(root.get("status"), LifecycleEntity.STATUS_ARCHIVED));
}
if (educationFormIds != null && !educationFormIds.isEmpty()) {
predicates.add(root.get("educationForm").get("id").in(educationFormIds));
}
if (departmentId != null) {
predicates.add(builder.equal(root.get("departmentId"), departmentId));
}
if (yearStartStudy != null) {
predicates.add(builder.equal(root.get("yearStartStudy"), yearStartStudy));
}
String normalizedQuery = normalizeQuery(query);
if (!normalizedQuery.isEmpty()) {
String pattern = "%" + normalizedQuery + "%";
predicates.add(builder.or(
builder.like(builder.lower(root.get("name")), pattern),
builder.like(builder.lower(root.get("speciality").get("specialityCode")), pattern),
builder.like(builder.lower(root.get("speciality").get("specialityName")), pattern),
builder.like(builder.lower(root.get("specialtyProfile").get("name")), pattern),
builder.like(builder.lower(root.get("educationForm").get("name")), pattern)
));
}
return builder.and(predicates.toArray(jakarta.persistence.criteria.Predicate[]::new));
};
}
private Sort groupSort(String field, String direction) {
String property = switch (field) {
case "yearStartStudy" -> "yearStartStudy";
case "groupSize" -> "groupSize";
case "status" -> "status";
default -> "name";
};
return "desc".equalsIgnoreCase(direction)
? Sort.by(property).descending()
: Sort.by(property).ascending();
}
private int normalizePage(int page) {
return Math.max(page, 0);
}
private int normalizeSize(int size) {
return Math.max(1, Math.min(size, 100));
}
private String normalizeQuery(String query) {
return query == null ? "" : query.trim().toLowerCase(Locale.ROOT);
}
@GetMapping("/{departmentId}")
public ResponseEntity<?> getGroupsByDepartmentId(@PathVariable Long departmentId) {
logger.info("Получен запрос на получение списка групп для кафедры с ID - {}", departmentId);

View File

@@ -12,10 +12,13 @@ import com.magistr.app.service.ScheduleGeneratorService;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Map;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
@RestController
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.STUDENT})
public class SubgroupController {
@@ -36,8 +39,16 @@ public class SubgroupController {
}
@GetMapping("/api/subgroups")
public List<SubgroupDto> getAll() {
return subgroupRepository.findAllWithGroupsOrderByGroupNameAndName().stream()
public List<SubgroupDto> getAll(@RequestParam(required = false) List<Long> groupId) {
if (groupId != null && groupId.size() > 100) {
throw new ResponseStatusException(BAD_REQUEST, "За один запрос можно получить подгруппы не более чем для 100 групп");
}
List<Subgroup> selected = groupId == null
? subgroupRepository.findAllWithGroupsOrderByGroupNameAndName()
: groupId.isEmpty()
? List.of()
: subgroupRepository.findByStudentGroupIdInOrderByGroupNameAndName(groupId.stream().distinct().toList());
return selected.stream()
.map(this::toDto)
.toList();
}

View File

@@ -1,8 +1,10 @@
package com.magistr.app.controller;
import com.magistr.app.config.auth.AuthContext;
import com.magistr.app.config.auth.RequireRoles;
import com.magistr.app.dto.CreateSubjectRequest;
import com.magistr.app.dto.SubjectResponse;
import com.magistr.app.dto.PageResponse;
import com.magistr.app.model.LifecycleEntity;
import com.magistr.app.model.Role;
import com.magistr.app.model.Subject;
@@ -11,9 +13,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@RestController
@@ -53,6 +60,74 @@ public class SubjectController {
}
}
@GetMapping("/page")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
public PageResponse<SubjectResponse> getSubjectsPage(
@RequestParam(defaultValue = "") String query,
@RequestParam(required = false) Long departmentId,
@RequestParam(defaultValue = "false") boolean includeArchived,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "25") int size,
@RequestParam(defaultValue = "name") String sort,
@RequestParam(defaultValue = "asc") String direction) {
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
var subjects = subjectRepository.findAll(
subjectSpecification(query, effectiveDepartmentId, includeArchived),
PageRequest.of(
Math.max(page, 0),
Math.max(1, Math.min(size, 100)),
subjectSort(sort, direction)
)
);
return PageResponse.from(subjects.map(this::toResponse));
}
private Long effectiveDepartmentId(Long requestedDepartmentId) {
var currentUser = AuthContext.getCurrentUser();
return currentUser != null && currentUser.role() == Role.DEPARTMENT
? currentUser.departmentId()
: requestedDepartmentId;
}
private Specification<Subject> subjectSpecification(String query,
Long departmentId,
boolean includeArchived) {
return (root, criteriaQuery, builder) -> {
List<jakarta.persistence.criteria.Predicate> predicates = new ArrayList<>();
if (!includeArchived) {
predicates.add(builder.notEqual(root.get("status"), LifecycleEntity.STATUS_ARCHIVED));
}
if (departmentId != null) {
predicates.add(builder.equal(root.get("departmentId"), departmentId));
}
String normalizedQuery = query == null ? "" : query.trim().toLowerCase(Locale.ROOT);
if (!normalizedQuery.isEmpty()) {
String pattern = "%" + normalizedQuery + "%";
predicates.add(builder.or(
builder.like(builder.lower(root.get("name")), pattern),
builder.like(builder.lower(root.get("code")), pattern)
));
}
return builder.and(predicates.toArray(jakarta.persistence.criteria.Predicate[]::new));
};
}
private Sort subjectSort(String field, String direction) {
String property = switch (field) {
case "code" -> "code";
case "departmentId" -> "departmentId";
case "status" -> "status";
default -> "name";
};
return "desc".equalsIgnoreCase(direction)
? Sort.by(property).descending()
: Sort.by(property).ascending();
}
private SubjectResponse toResponse(Subject subject) {
return new SubjectResponse(subject.getId(), subject.getName(), subject.getCode(), subject.getDepartmentId());
}
@GetMapping("/{departmentId}")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
public ResponseEntity<?> getSubjectsByDepartmentId(@PathVariable Long departmentId) {

View File

@@ -5,6 +5,7 @@ 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.dto.PageResponse;
import com.magistr.app.model.*;
import com.magistr.app.repository.DepartmentRepository;
import com.magistr.app.repository.TeacherCreationRequestRepository;
@@ -12,11 +13,16 @@ import com.magistr.app.repository.TeacherDepartmentAssignmentRepository;
import com.magistr.app.repository.UserRepository;
import com.magistr.app.service.BusinessTimeService;
import org.springframework.http.ResponseEntity;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@RestController
@@ -55,6 +61,63 @@ public class TeacherCreationRequestController {
.toList();
}
@GetMapping("/page")
public PageResponse<TeacherCreationRequestResponse> getRequestsPage(
@RequestParam(defaultValue = "") String query,
@RequestParam(required = false) TeacherCreationRequestStatus status,
@RequestParam(required = false) Long departmentId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "25") int size,
@RequestParam(defaultValue = "createdAt") String sort,
@RequestParam(defaultValue = "desc") String direction) {
var requests = teacherCreationRequestRepository.findAll(
requestSpecification(query, status, departmentId),
PageRequest.of(
Math.max(page, 0),
Math.max(1, Math.min(size, 100)),
requestSort(sort, direction)
)
);
return PageResponse.from(requests.map(this::toResponse));
}
private Specification<TeacherCreationRequest> requestSpecification(String query,
TeacherCreationRequestStatus status,
Long departmentId) {
return (root, criteriaQuery, builder) -> {
List<jakarta.persistence.criteria.Predicate> predicates = new ArrayList<>();
if (status != null) {
predicates.add(builder.equal(root.get("status"), status));
}
if (departmentId != null) {
predicates.add(builder.equal(root.get("department").get("id"), departmentId));
}
String normalizedQuery = query == null ? "" : query.trim().toLowerCase(Locale.ROOT);
if (!normalizedQuery.isEmpty()) {
String pattern = "%" + normalizedQuery + "%";
predicates.add(builder.or(
builder.like(builder.lower(root.get("username")), pattern),
builder.like(builder.lower(root.get("fullName")), pattern),
builder.like(builder.lower(root.get("jobTitle")), pattern),
builder.like(builder.lower(root.get("department").get("departmentName")), pattern)
));
}
return builder.and(predicates.toArray(jakarta.persistence.criteria.Predicate[]::new));
};
}
private Sort requestSort(String field, String direction) {
String property = switch (field) {
case "fullName" -> "fullName";
case "username" -> "username";
case "status" -> "status";
default -> "createdAt";
};
return "asc".equalsIgnoreCase(direction)
? Sort.by(property).ascending()
: Sort.by(property).descending();
}
@PostMapping("/{id}/approve")
@Transactional
public ResponseEntity<?> approve(@PathVariable Long id, @RequestBody ReviewTeacherCreationRequest review) {

View File

@@ -16,10 +16,13 @@ import com.magistr.app.service.BusinessTimeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Map;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
@RestController
@RequestMapping("/api/teacher-subjects")
@RequireRoles({Role.ADMIN, Role.DEPARTMENT})
@@ -53,15 +56,18 @@ public class TeacherSubjectController {
}
@GetMapping
public List<TeacherSubjectResponse> getAll() {
return teacherSubjectRepository.findAll().stream()
public List<TeacherSubjectResponse> getAll(@RequestParam(required = false) List<Long> subjectId) {
if (subjectId != null && subjectId.size() > 100) {
throw new ResponseStatusException(BAD_REQUEST, "За один запрос можно получить привязки не более чем для 100 дисциплин");
}
List<TeacherSubject> assignments = subjectId == null
? teacherSubjectRepository.findAll()
: subjectId.isEmpty()
? List.of()
: teacherSubjectRepository.findBySubjectIdInWithDetails(subjectId.stream().distinct().toList());
return assignments.stream()
.filter(ts -> canManage(ts.getUser(), ts.getSubject()))
.map(ts -> new TeacherSubjectResponse(
ts.getUserId(),
ts.getUser().getUsername(),
ts.getSubjectId(),
ts.getSubject().getName()
))
.map(this::toResponse)
.toList();
}
@@ -136,4 +142,14 @@ public class TeacherSubjectController {
)
&& departmentId.equals(subject.getDepartmentId());
}
private TeacherSubjectResponse toResponse(TeacherSubject teacherSubject) {
return new TeacherSubjectResponse(
teacherSubject.getUserId(),
teacherSubject.getUser().getUsername(),
teacherSubject.getUser().getFullName(),
teacherSubject.getSubjectId(),
teacherSubject.getSubject().getName()
);
}
}

View File

@@ -6,6 +6,7 @@ import com.magistr.app.dto.CreateUserRequest;
import com.magistr.app.dto.DepartmentTransferRequest;
import com.magistr.app.dto.TeacherDepartmentAssignmentDto;
import com.magistr.app.dto.UserResponse;
import com.magistr.app.dto.PageResponse;
import com.magistr.app.model.Department;
import com.magistr.app.model.LifecycleEntity;
import com.magistr.app.model.Role;
@@ -22,11 +23,16 @@ import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@RestController
@@ -80,6 +86,23 @@ public class UserController {
return response;
}
@GetMapping("/page")
public PageResponse<UserResponse> getUsersPage(
@RequestParam(defaultValue = "") String query,
@RequestParam(required = false) Role role,
@RequestParam(required = false) Long departmentId,
@RequestParam(defaultValue = "true") boolean includeArchived,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "25") int size,
@RequestParam(defaultValue = "fullName") String sort,
@RequestParam(defaultValue = "asc") String direction) {
var users = userRepository.findAll(
userSpecification(query, role, departmentId, includeArchived),
PageRequest.of(normalizePage(page), normalizeSize(size), userSort(sort, direction))
);
return PageResponse.from(users.map(this::toUserResponse));
}
@GetMapping("/teachers")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
public List<UserResponse> getTeachers() {
@@ -91,6 +114,92 @@ public class UserController {
return response;
}
@GetMapping("/teachers/options")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
public List<UserResponse> searchTeacherOptions(
@RequestParam(defaultValue = "") String query,
@RequestParam(required = false) Long departmentId,
@RequestParam(required = false) Long selectedId,
@RequestParam(defaultValue = "20") int limit) {
var currentUser = AuthContext.getCurrentUser();
Long effectiveDepartmentId = currentUser != null && currentUser.role() == Role.DEPARTMENT
? currentUser.departmentId()
: departmentId;
LocalDate today = businessTime.today();
List<UserResponse> options = new ArrayList<>(userRepository.searchTeacherOptions(
query == null ? "" : query.trim().toLowerCase(Locale.ROOT),
Role.TEACHER,
LifecycleEntity.STATUS_ARCHIVED,
effectiveDepartmentId,
today,
PageRequest.of(0, Math.min(normalizeSize(limit), 50), Sort.by("fullName").ascending())
).stream()
.map(user -> effectiveDepartmentId == null
? toUserResponse(user)
: toUserResponse(user, effectiveDepartmentId))
.toList());
if (selectedId != null && options.stream().noneMatch(item -> selectedId.equals(item.getId()))) {
userRepository.findById(selectedId)
.filter(user -> user.getRole() == Role.TEACHER)
.filter(user -> !user.isArchivedRecord())
.filter(user -> effectiveDepartmentId == null
|| teacherDepartmentService.hasAssignmentAtDate(user.getId(), effectiveDepartmentId, today))
.map(user -> effectiveDepartmentId == null
? toUserResponse(user)
: toUserResponse(user, effectiveDepartmentId))
.ifPresent(options::add);
}
return options;
}
private Specification<User> userSpecification(String query,
Role role,
Long departmentId,
boolean includeArchived) {
return (root, criteriaQuery, builder) -> {
List<jakarta.persistence.criteria.Predicate> predicates = new ArrayList<>();
if (!includeArchived) {
predicates.add(builder.notEqual(root.get("status"), LifecycleEntity.STATUS_ARCHIVED));
}
if (role != null) {
predicates.add(builder.equal(root.get("role"), role));
}
if (departmentId != null) {
predicates.add(builder.equal(root.get("departmentId"), departmentId));
}
String normalizedQuery = query == null ? "" : query.trim().toLowerCase(Locale.ROOT);
if (!normalizedQuery.isEmpty()) {
String pattern = "%" + normalizedQuery + "%";
predicates.add(builder.or(
builder.like(builder.lower(root.get("username")), pattern),
builder.like(builder.lower(root.get("fullName")), pattern),
builder.like(builder.lower(root.get("jobTitle")), pattern)
));
}
return builder.and(predicates.toArray(jakarta.persistence.criteria.Predicate[]::new));
};
}
private Sort userSort(String field, String direction) {
String property = switch (field) {
case "username" -> "username";
case "role" -> "role";
case "status" -> "status";
default -> "fullName";
};
return "desc".equalsIgnoreCase(direction)
? Sort.by(property).descending()
: Sort.by(property).ascending();
}
private int normalizePage(int page) {
return Math.max(page, 0);
}
private int normalizeSize(int size) {
return Math.max(1, Math.min(size, 100));
}
@GetMapping("/teachers/{departmentId}")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
public ResponseEntity<?> getTeachersByDepartmentId(@PathVariable Long departmentId){

View File

@@ -0,0 +1,27 @@
package com.magistr.app.dto;
import org.springframework.data.domain.Page;
import java.util.List;
public record PageResponse<T>(
List<T> items,
int page,
int size,
long totalItems,
int totalPages,
boolean first,
boolean last
) {
public static <T> PageResponse<T> from(Page<T> page) {
return new PageResponse<>(
page.getContent(),
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isFirst(),
page.isLast()
);
}
}

View File

@@ -4,12 +4,14 @@ public class TeacherSubjectResponse {
private Long userId;
private String username;
private String fullName;
private Long subjectId;
private String subjectName;
public TeacherSubjectResponse(Long userId, String username, Long subjectId, String subjectName) {
public TeacherSubjectResponse(Long userId, String username, String fullName, Long subjectId, String subjectName) {
this.userId = userId;
this.username = username;
this.fullName = fullName;
this.subjectId = subjectId;
this.subjectName = subjectName;
}
@@ -22,6 +24,10 @@ public class TeacherSubjectResponse {
return username;
}
public String getFullName() {
return fullName;
}
public Long getSubjectId() {
return subjectId;
}

View File

@@ -3,14 +3,17 @@ package com.magistr.app.repository;
import com.magistr.app.model.AcademicCalendar;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
public interface AcademicCalendarRepository extends JpaRepository<AcademicCalendar, Long> {
public interface AcademicCalendarRepository extends JpaRepository<AcademicCalendar, Long>, JpaSpecificationExecutor<AcademicCalendar> {
long countByStudyFormId(Long studyFormId);
@@ -54,4 +57,49 @@ public interface AcademicCalendarRepository extends JpaRepository<AcademicCalend
where calendar.id = :id
""")
Optional<AcademicCalendar> findByIdWithDetailsForUpdate(@Param("id") Long id);
@Query(value = """
select calendar
from AcademicCalendar calendar
join fetch calendar.academicYear academicYear
join fetch calendar.speciality speciality
join fetch calendar.specialtyProfile specialtyProfile
join fetch calendar.studyForm studyForm
where (:query = ''
or lower(calendar.title) like concat('%', :query, '%')
or lower(academicYear.title) like concat('%', :query, '%')
or lower(speciality.specialityCode) like concat('%', :query, '%')
or lower(speciality.specialityName) like concat('%', :query, '%')
or lower(specialtyProfile.name) like concat('%', :query, '%')
or lower(studyForm.name) like concat('%', :query, '%'))
and (:academicYearId is null or academicYear.id = :academicYearId)
and (:specialtyId is null or speciality.id = :specialtyId)
and (:profileId is null or specialtyProfile.id = :profileId)
and (:studyFormId is null or studyForm.id = :studyFormId)
""",
countQuery = """
select count(calendar)
from AcademicCalendar calendar
join calendar.academicYear academicYear
join calendar.speciality speciality
join calendar.specialtyProfile specialtyProfile
join calendar.studyForm studyForm
where (:query = ''
or lower(calendar.title) like concat('%', :query, '%')
or lower(academicYear.title) like concat('%', :query, '%')
or lower(speciality.specialityCode) like concat('%', :query, '%')
or lower(speciality.specialityName) like concat('%', :query, '%')
or lower(specialtyProfile.name) like concat('%', :query, '%')
or lower(studyForm.name) like concat('%', :query, '%'))
and (:academicYearId is null or academicYear.id = :academicYearId)
and (:specialtyId is null or speciality.id = :specialtyId)
and (:profileId is null or specialtyProfile.id = :profileId)
and (:studyFormId is null or studyForm.id = :studyFormId)
""")
Page<AcademicCalendar> searchOptions(@Param("query") String query,
@Param("academicYearId") Long academicYearId,
@Param("specialtyId") Long specialtyId,
@Param("profileId") Long profileId,
@Param("studyFormId") Long studyFormId,
Pageable pageable);
}

View File

@@ -2,12 +2,29 @@ package com.magistr.app.repository;
import com.magistr.app.model.Classroom;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.Optional;
import java.util.List;
public interface ClassroomRepository extends JpaRepository<Classroom, Long> {
public interface ClassroomRepository extends JpaRepository<Classroom, Long>, JpaSpecificationExecutor<Classroom> {
Optional<Classroom> findByName(String name);
List<Classroom> findByStatusNot(String status);
@Query("""
select classroom
from Classroom classroom
where classroom.status <> :archivedStatus
and (:query = ''
or lower(classroom.name) like concat('%', :query, '%')
or lower(coalesce(classroom.building, '')) like concat('%', :query, '%'))
""")
Page<Classroom> searchActiveOptions(@Param("query") String query,
@Param("archivedStatus") String archivedStatus,
Pageable pageable);
}

View File

@@ -3,6 +3,7 @@ package com.magistr.app.repository;
import com.magistr.app.model.StudentGroup;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -10,7 +11,7 @@ import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
public interface GroupRepository extends JpaRepository<StudentGroup, Long> {
public interface GroupRepository extends JpaRepository<StudentGroup, Long>, JpaSpecificationExecutor<StudentGroup> {
List<StudentGroup> findByEducationFormId(Long educationFormId);

View File

@@ -35,6 +35,16 @@ public interface SubgroupRepository extends JpaRepository<Subgroup, Long> {
""")
List<Subgroup> findByStudentGroupIdOrderByNameAsc(@Param("groupId") Long groupId);
@Query("""
select subgroup
from Subgroup subgroup
join fetch subgroup.studentGroup studentGroup
where studentGroup.id in :groupIds
and subgroup.status <> 'ARCHIVED'
order by studentGroup.name, subgroup.name
""")
List<Subgroup> findByStudentGroupIdInOrderByGroupNameAndName(@Param("groupIds") List<Long> groupIds);
@Query("""
select subgroup
from Subgroup subgroup

View File

@@ -2,11 +2,12 @@ package com.magistr.app.repository;
import com.magistr.app.model.Subject;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.util.List;
import java.util.Optional;
public interface SubjectRepository extends JpaRepository<Subject, Long> {
public interface SubjectRepository extends JpaRepository<Subject, Long>, JpaSpecificationExecutor<Subject> {
Optional<Subject> findByNameIgnoreCase(String name);
List<Subject> findByDepartmentId(Long departmentId);

View File

@@ -3,10 +3,11 @@ 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 org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.util.List;
public interface TeacherCreationRequestRepository extends JpaRepository<TeacherCreationRequest, Long> {
public interface TeacherCreationRequestRepository extends JpaRepository<TeacherCreationRequest, Long>, JpaSpecificationExecutor<TeacherCreationRequest> {
List<TeacherCreationRequest> findByDepartment_IdOrderByCreatedAtDesc(Long departmentId);

View File

@@ -12,6 +12,16 @@ public interface TeacherSubjectRepository extends JpaRepository<TeacherSubject,
List<TeacherSubject> findByUserId(Long userId);
List<TeacherSubject> findBySubjectId(Long subjectId);
@Query("""
select teacherSubject
from TeacherSubject teacherSubject
join fetch teacherSubject.user
join fetch teacherSubject.subject
where teacherSubject.subjectId in :subjectIds
order by teacherSubject.subject.name, teacherSubject.user.fullName
""")
List<TeacherSubject> findBySubjectIdInWithDetails(@Param("subjectIds") List<Long> subjectIds);
@Query(value = """
select subject_link.user_id
from teacher_subjects subject_link

View File

@@ -4,14 +4,18 @@ import com.magistr.app.model.Role;
import com.magistr.app.model.User;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
Optional<User> findByUsername(String username);
@@ -25,6 +29,49 @@ public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByRoleAndDepartmentIdAndStatusNot(Role role, Long departmentId, String status);
@Query(value = """
select user
from User user
where user.role = :role
and user.status <> :archivedStatus
and (:query = ''
or lower(user.username) like concat('%', :query, '%')
or lower(user.fullName) like concat('%', :query, '%')
or lower(coalesce(user.jobTitle, '')) like concat('%', :query, '%'))
and (:departmentId is null or exists (
select assignment.id
from TeacherDepartmentAssignment assignment
where assignment.teacher.id = user.id
and assignment.department.id = :departmentId
and assignment.validFrom <= :date
and (assignment.validTo is null or assignment.validTo >= :date)
))
""",
countQuery = """
select count(user)
from User user
where user.role = :role
and user.status <> :archivedStatus
and (:query = ''
or lower(user.username) like concat('%', :query, '%')
or lower(user.fullName) like concat('%', :query, '%')
or lower(coalesce(user.jobTitle, '')) like concat('%', :query, '%'))
and (:departmentId is null or exists (
select assignment.id
from TeacherDepartmentAssignment assignment
where assignment.teacher.id = user.id
and assignment.department.id = :departmentId
and assignment.validFrom <= :date
and (assignment.validTo is null or assignment.validTo >= :date)
))
""")
Page<User> searchTeacherOptions(@Param("query") String query,
@Param("role") Role role,
@Param("archivedStatus") String archivedStatus,
@Param("departmentId") Long departmentId,
@Param("date") LocalDate date,
Pageable pageable);
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select user from User user where user.id = :id")
Optional<User> findByIdForUpdate(@Param("id") Long id);

View File

@@ -18,6 +18,7 @@ import org.mockito.ArgumentCaptor;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -74,6 +75,52 @@ class TeacherSubjectControllerTest {
assertThat(captor.getValue().getSubjectId()).isEqualTo(subject.getId());
}
@Test
void returnsAssignmentsOnlyForRequestedSubjects() {
TeacherSubjectRepository teacherSubjectRepository = mock(TeacherSubjectRepository.class);
UserRepository userRepository = mock(UserRepository.class);
SubjectRepository subjectRepository = mock(SubjectRepository.class);
TeacherDepartmentService teacherDepartmentService = mock(TeacherDepartmentService.class);
User teacher = teacher();
Subject subject = new Subject();
subject.setId(20L);
subject.setName("Высшая математика");
TeacherSubject assignment = new TeacherSubject(teacher.getId(), subject.getId());
setTeacherSubjectDetails(assignment, teacher, subject);
when(teacherSubjectRepository.findBySubjectIdInWithDetails(List.of(subject.getId())))
.thenReturn(List.of(assignment));
TeacherSubjectController controller = new TeacherSubjectController(
teacherSubjectRepository,
userRepository,
subjectRepository,
teacherDepartmentService,
BUSINESS_TIME
);
AuthContext.setCurrentUser(new AuthenticatedUser(1L, "admin", Role.ADMIN, null));
var result = controller.getAll(List.of(subject.getId()));
assertThat(result).singleElement().satisfies(response -> {
assertThat(response.getSubjectId()).isEqualTo(subject.getId());
assertThat(response.getUserId()).isEqualTo(teacher.getId());
assertThat(response.getFullName()).isEqualTo(teacher.getFullName());
});
verify(teacherSubjectRepository).findBySubjectIdInWithDetails(List.of(subject.getId()));
}
private void setTeacherSubjectDetails(TeacherSubject assignment, User teacher, Subject subject) {
try {
var userField = TeacherSubject.class.getDeclaredField("user");
userField.setAccessible(true);
userField.set(assignment, teacher);
var subjectField = TeacherSubject.class.getDeclaredField("subject");
subjectField.setAccessible(true);
subjectField.set(assignment, subject);
} catch (ReflectiveOperationException exception) {
throw new AssertionError("Не удалось подготовить тестовую привязку", exception);
}
}
private User teacher() {
User teacher = new User();
teacher.setId(10L);