GroupCreation, ErrorHandling, Database
This commit is contained in:
parent
c2d19a7724
commit
b1ea3a670e
1
.gitignore
vendored
1
.gitignore
vendored
@ -31,3 +31,4 @@ build/
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/logs/app.log
|
||||
/configurations/Start RDBMS.run.xml
|
||||
|
||||
@ -97,6 +97,7 @@ public class SecurityConfiguration {
|
||||
httpAuthorization.requestMatchers("/api/v1/student/current").permitAll();
|
||||
/* GroupController */
|
||||
httpAuthorization.requestMatchers("/api/v1/group/get-all").permitAll();
|
||||
httpAuthorization.requestMatchers("api/v1/group/create-group").hasAuthority("ROLE_ADMINISTRATOR");
|
||||
/* deny all other api requests */
|
||||
httpAuthorization.requestMatchers("/api/**").denyAll();
|
||||
/* since api already blocked, all other requests are static resources */
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
package ru.tubryansk.tdms.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.tubryansk.tdms.controller.payload.GroupDTO;
|
||||
import ru.tubryansk.tdms.controller.payload.GroupRegistrationDTO;
|
||||
import ru.tubryansk.tdms.service.GroupService;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@RestController("/api/v1/group/")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/group")
|
||||
public class GroupController {
|
||||
@Autowired
|
||||
private GroupService groupService;
|
||||
@ -17,4 +19,9 @@ public class GroupController {
|
||||
public Collection<GroupDTO> getAllGroups() {
|
||||
return groupService.getAllGroups();
|
||||
}
|
||||
|
||||
@PostMapping("/create-group")
|
||||
public void createGroup(@RequestBody @Valid GroupRegistrationDTO groupRegistrationDTO) {
|
||||
groupService.createGroup(groupRegistrationDTO.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package ru.tubryansk.tdms.controller;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@ -13,9 +12,10 @@ public class SysInfoController {
|
||||
@Autowired
|
||||
private SysInfoService sysInfoService;
|
||||
|
||||
@SneakyThrows
|
||||
@GetMapping("/version")
|
||||
public String getVersion() {
|
||||
return sysInfoService.getVersion();
|
||||
}
|
||||
|
||||
// @GetMapping("/status")
|
||||
}
|
||||
|
||||
@ -9,6 +9,6 @@ import lombok.ToString;
|
||||
@ToString
|
||||
public class GroupDTO {
|
||||
private String name;
|
||||
private String principalName;
|
||||
private Boolean isMePrincipal;
|
||||
private String curatorName;
|
||||
private Boolean iAmCurator;
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
package ru.tubryansk.tdms.controller.payload;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class GroupRegistrationDTO {
|
||||
@NotEmpty(message = "Имя группы не может быть пустым")
|
||||
@Size(min = 3, max = 50, message = "Имя группы должно быть от 3 до 50 символов")
|
||||
@Pattern(regexp = "^[а-яА-ЯёЁ0-9_-]*$", message = "Имя группы должно содержать только русские буквы, дефис, нижнее подчеркивание и цифры")
|
||||
private String name;
|
||||
}
|
||||
@ -5,13 +5,17 @@ import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@Entity
|
||||
@Table(name = "group")
|
||||
@Table(name = "`group`")
|
||||
public class Group {
|
||||
@Id
|
||||
@Column(name = "id")
|
||||
@ -20,6 +24,12 @@ public class Group {
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "curator_user_id")
|
||||
private User groupCurator;
|
||||
@JoinColumn(name = "curator_teacher_id")
|
||||
private Teacher groupCurator;
|
||||
@Column(name = "created_at")
|
||||
@CreationTimestamp
|
||||
private ZonedDateTime createdAt;
|
||||
@Column(name = "updated_at")
|
||||
@UpdateTimestamp
|
||||
private ZonedDateTime updatedAt;
|
||||
}
|
||||
|
||||
@ -47,9 +47,10 @@ public class Student {
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "diploma_topic_id")
|
||||
private DiplomaTopic diplomaTopic;
|
||||
// Научный руководитель
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "mentor_user_id")
|
||||
private User mentorUser;
|
||||
@JoinColumn(name = "adviser_teacher_id")
|
||||
private Teacher adviser;
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "group_id")
|
||||
private Group group;
|
||||
|
||||
32
server/src/main/java/ru/tubryansk/tdms/entity/Teacher.java
Normal file
32
server/src/main/java/ru/tubryansk/tdms/entity/Teacher.java
Normal file
@ -0,0 +1,32 @@
|
||||
package ru.tubryansk.tdms.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "teacher")
|
||||
public class Teacher {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
@OneToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
@OneToMany(mappedBy = "groupCurator")
|
||||
private List<Group> curatingGroups;
|
||||
@OneToMany(mappedBy = "adviser")
|
||||
private List<Student> advisingStudents;
|
||||
|
||||
@Column(name = "created_at")
|
||||
@CreationTimestamp
|
||||
private ZonedDateTime createdAt;
|
||||
@Column(name = "updated_at")
|
||||
@UpdateTimestamp
|
||||
private ZonedDateTime updatedAt;
|
||||
}
|
||||
@ -2,6 +2,7 @@ package ru.tubryansk.tdms.entity;
|
||||
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
@ -19,6 +20,7 @@ import java.util.List;
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@EqualsAndHashCode(of = "id")
|
||||
@Entity
|
||||
@Table(name = "`user`")
|
||||
public class User implements UserDetails {
|
||||
|
||||
@ -10,4 +10,6 @@ public interface GroupRepository extends JpaRepository<Group, Long> {
|
||||
default Group findByIdThrow(Long id) {
|
||||
return this.findById(id).orElseThrow(() -> new NotFoundException(Group.class, id));
|
||||
}
|
||||
|
||||
boolean existsByName(String name);
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@ import ru.tubryansk.tdms.exception.NotFoundException;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface StudentRepository extends JpaRepository<Student, Integer> {
|
||||
default Student findByIdThrow(Integer id) {
|
||||
public interface StudentRepository extends JpaRepository<Student, Long> {
|
||||
default Student findByIdThrow(Long id) {
|
||||
return this.findById(id).orElseThrow(() -> new NotFoundException(Student.class, id));
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package ru.tubryansk.tdms.entity.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import ru.tubryansk.tdms.entity.Teacher;
|
||||
import ru.tubryansk.tdms.exception.NotFoundException;
|
||||
|
||||
@Repository
|
||||
public interface TeacherRepository extends JpaRepository<Teacher, Long> {
|
||||
default Teacher findByIdThrow(Long id) {
|
||||
return this.findById(id).orElseThrow(() -> new NotFoundException(Teacher.class, id));
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
import ru.tubryansk.tdms.controller.payload.ErrorResponse;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestControllerAdvice
|
||||
@ -51,7 +52,9 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public ErrorResponse handleUnexpectedException(Exception e) {
|
||||
log.error("Unexpected exception.", e);
|
||||
return new ErrorResponse(e.getMessage(), ErrorResponse.ErrorCode.INTERNAL_ERROR);
|
||||
Random random = new Random();
|
||||
long errorInx = random.nextLong();
|
||||
log.error("Unexpected exception. random: {}", errorInx, e);
|
||||
return new ErrorResponse("Произошла непредвиденная ошибка, обратитесь к администратору. Номер ошибки: " + errorInx, ErrorResponse.ErrorCode.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,53 @@
|
||||
package ru.tubryansk.tdms.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.tubryansk.tdms.controller.payload.GroupDTO;
|
||||
import ru.tubryansk.tdms.entity.Group;
|
||||
import ru.tubryansk.tdms.entity.User;
|
||||
import ru.tubryansk.tdms.entity.repository.GroupRepository;
|
||||
import ru.tubryansk.tdms.exception.BusinessException;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class GroupService {
|
||||
@Autowired
|
||||
private GroupRepository groupRepository;
|
||||
@Autowired
|
||||
private CallerService callerService;
|
||||
|
||||
public Collection<GroupDTO> getAllGroups() {
|
||||
return null;
|
||||
List<Group> groups = groupRepository.findAll();
|
||||
User callerUser = callerService.getCallerUser().orElse(null);
|
||||
|
||||
return groups.stream()
|
||||
.map(g -> {
|
||||
GroupDTO groupDTO = new GroupDTO();
|
||||
groupDTO.setName(g.getName());
|
||||
|
||||
if (g.getGroupCurator() != null) {
|
||||
groupDTO.setCuratorName(g.getGroupCurator().getUser().getFullName());
|
||||
if (callerUser != null) {
|
||||
groupDTO.setIAmCurator(g.getGroupCurator().getUser().equals(callerUser));
|
||||
}
|
||||
}
|
||||
return groupDTO;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
public void createGroup(String groupName) {
|
||||
boolean existsByName = groupRepository.existsByName(groupName);
|
||||
if (existsByName) {
|
||||
throw new BusinessException("Группа с именем " + groupName + " уже существует");
|
||||
}
|
||||
|
||||
Group group = new Group();
|
||||
group.setName(groupName);
|
||||
groupRepository.save(group);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import ru.tubryansk.tdms.entity.repository.DiplomaTopicRepository;
|
||||
import ru.tubryansk.tdms.entity.repository.StudentRepository;
|
||||
import ru.tubryansk.tdms.exception.AccessDeniedException;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@ -26,15 +25,15 @@ public class StudentService {
|
||||
private CallerService callerService;
|
||||
|
||||
/** @param studentToDiplomaTopic Map of @{@link Student} id and @{@link DiplomaTopic} id */
|
||||
public void changeDiplomaTopic(Map<Integer, Integer> studentToDiplomaTopic) {
|
||||
studentToDiplomaTopic.forEach(this::changeDiplomaTopic);
|
||||
}
|
||||
// public void changeDiplomaTopic(Map<Integer, Integer> studentToDiplomaTopic) {
|
||||
// studentToDiplomaTopic.forEach(this::changeDiplomaTopic);
|
||||
// }
|
||||
|
||||
public void changeDiplomaTopic(Integer studentId, Integer diplomaTopicId) {
|
||||
Student student = studentRepository.findByIdThrow(studentId);
|
||||
DiplomaTopic diplomaTopic = diplomaTopicRepository.findByIdThrow(diplomaTopicId);
|
||||
student.setDiplomaTopic(diplomaTopic);
|
||||
}
|
||||
// public void changeDiplomaTopic(Integer studentId, Integer diplomaTopicId) {
|
||||
// Student student = studentRepository.findByIdThrow(studentId);
|
||||
// DiplomaTopic diplomaTopic = diplomaTopicRepository.findByIdThrow(diplomaTopicId);
|
||||
// student.setDiplomaTopic(diplomaTopic);
|
||||
// }
|
||||
|
||||
public void changeCallerDiplomaTopic(Integer diplomaTopicId) {
|
||||
DiplomaTopic diplomaTopic = diplomaTopicRepository.findByIdThrow(diplomaTopicId);
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
create table "group"
|
||||
(
|
||||
id bigserial primary key,
|
||||
|
||||
name text not null unique,
|
||||
curator_user_id bigint,
|
||||
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz
|
||||
);
|
||||
|
||||
-- FOREIGN KEY
|
||||
alter table "group"
|
||||
add constraint fk_group_curator_user_id
|
||||
foreign key (curator_user_id) references "user" (id)
|
||||
on delete set null on update cascade;
|
||||
|
||||
-- COMMENTS
|
||||
comment on table "group" is 'Таблица групп студентов';
|
||||
|
||||
comment on column "group".name is 'Название группы';
|
||||
comment on column "group".curator_user_id is 'Идентификатор куратора группы';
|
||||
@ -0,0 +1,19 @@
|
||||
create table teacher
|
||||
(
|
||||
id bigserial primary key,
|
||||
user_id bigint not null,
|
||||
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz
|
||||
);
|
||||
|
||||
-- FOREIGN KEY
|
||||
alter table teacher
|
||||
add constraint fk_teacher_user_id
|
||||
foreign key (user_id) references "user" (id)
|
||||
on delete cascade on update cascade;
|
||||
|
||||
-- COMMENTS
|
||||
comment on table teacher is 'Таблица преподавателей';
|
||||
|
||||
comment on column teacher.user_id is 'Идентификатор пользователя';
|
||||
@ -0,0 +1,22 @@
|
||||
create table "group"
|
||||
(
|
||||
id bigserial primary key,
|
||||
|
||||
name text not null unique,
|
||||
curator_teacher_id bigint,
|
||||
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz
|
||||
);
|
||||
|
||||
-- FOREIGN KEY
|
||||
alter table "group"
|
||||
add constraint fk_group_curator_teacher_id
|
||||
foreign key (curator_teacher_id) references teacher (id)
|
||||
on delete set null on update cascade;
|
||||
|
||||
-- COMMENTS
|
||||
comment on table "group" is 'Таблица групп студентов';
|
||||
|
||||
comment on column "group".name is 'Название группы';
|
||||
comment on column "group".curator_teacher_id is 'Идентификатор куратора группы';
|
||||
@ -1,18 +1,18 @@
|
||||
create table student
|
||||
(
|
||||
id bigserial primary key,
|
||||
user_id bigint not null,
|
||||
diploma_topic_id bigint not null,
|
||||
mentor_user_id bigint not null,
|
||||
group_id bigint not null,
|
||||
user_id bigint not null,
|
||||
diploma_topic_id bigint not null,
|
||||
adviser_teacher_id bigint not null,
|
||||
group_id bigint not null,
|
||||
|
||||
form boolean,
|
||||
protection_day int,
|
||||
protection_order int,
|
||||
protection_day int,
|
||||
protection_order int,
|
||||
magistracy text,
|
||||
digital_format_present boolean,
|
||||
mark_comment int,
|
||||
mark_practice int,
|
||||
mark_comment int,
|
||||
mark_practice int,
|
||||
predefence_comment text,
|
||||
normal_control text,
|
||||
anti_plagiarism int,
|
||||
@ -34,8 +34,8 @@ alter table student
|
||||
foreign key (diploma_topic_id) references diploma_topic (id)
|
||||
on delete set null on update cascade;
|
||||
alter table student
|
||||
add constraint fk_student_mentor_user_id
|
||||
foreign key (mentor_user_id) references "user" (id)
|
||||
add constraint fk_student_adviser_teacher_id
|
||||
foreign key (adviser_teacher_id) references teacher (id)
|
||||
on delete set null on update cascade;
|
||||
alter table student
|
||||
add constraint fk_student_group_id
|
||||
@ -47,7 +47,7 @@ comment on table student is 'Таблица студентов';
|
||||
|
||||
comment on column student.user_id is 'Идентификатор пользователя';
|
||||
comment on column student.diploma_topic_id is 'Идентификатор темы дипломной работы';
|
||||
comment on column student.mentor_user_id is 'Идентификатор научного руководителя';
|
||||
comment on column student.adviser_teacher_id is 'Идентификатор научного руководителя';
|
||||
comment on column student.group_id is 'Идентификатор группы';
|
||||
|
||||
comment on column student.form is 'Форма обучения';
|
||||
@ -1,7 +1,7 @@
|
||||
import {ComponentContext} from "../utils/ComponentContext";
|
||||
import {observer} from "mobx-react";
|
||||
import {Notification, NotificationType} from "../store/NotificationStore";
|
||||
import {Card, CardBody, CardHeader, CardText, CardTitle, Col, Row} from "react-bootstrap";
|
||||
import {Card, CardBody, CardHeader, CardText, CardTitle} from "react-bootstrap";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {action, makeObservable} from "mobx";
|
||||
|
||||
@ -14,7 +14,7 @@ export class NotificationContainer extends ComponentContext {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{position: 'fixed', left: '50%', transform: 'translateX(-50%)', zIndex: 1000}}>
|
||||
return <div style={{position: 'fixed', left: '50%', transform: 'translateX(-50%)', zIndex: 2000}}>
|
||||
{this.forEachNotificationRender(this.notificationStore.errors, NotificationType.ERROR)}
|
||||
{this.forEachNotificationRender(this.notificationStore.successes, NotificationType.SUCCESS)}
|
||||
{this.forEachNotificationRender(this.notificationStore.warnings, NotificationType.WARNING)}
|
||||
@ -51,37 +51,25 @@ class NotificationPopup extends ComponentContext<{ notification: Notification, t
|
||||
|
||||
render() {
|
||||
const hasTitle = !!this.props.notification.title && this.props.notification.title.length > 0;
|
||||
const closeIcon = <FontAwesomeIcon icon={'close'} onClick={this.close}/>;
|
||||
const closeIcon = <span className={'ms-2'}><FontAwesomeIcon icon={'close'} onClick={this.close}/></span>;
|
||||
|
||||
return <Card className={`position-relative mt-3 opacity-75 ${this.cardClassName}`}>
|
||||
{
|
||||
hasTitle &&
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Row>
|
||||
<Col sm={11}>
|
||||
{this.props.notification.title}
|
||||
</Col>
|
||||
<Col className={'text-end'}>
|
||||
{closeIcon}
|
||||
</Col>
|
||||
</Row>
|
||||
<CardTitle className={'d-flex justify-content-between align-items-start'}>
|
||||
{this.props.notification.title}
|
||||
{closeIcon}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
}
|
||||
<CardBody>
|
||||
<CardText>
|
||||
<Row>
|
||||
<Col sm={11}>
|
||||
{this.props.notification.message}
|
||||
</Col>
|
||||
{
|
||||
!hasTitle &&
|
||||
<Col className={'text-end'}>
|
||||
{closeIcon}
|
||||
</Col>
|
||||
}
|
||||
</Row>
|
||||
<CardText className={'d-flex justify-content-between align-items-start'}>
|
||||
{this.props.notification.message}
|
||||
{
|
||||
!hasTitle &&
|
||||
closeIcon
|
||||
}
|
||||
</CardText>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
@ -74,7 +74,7 @@ export class PasswordInput extends React.Component<ReactiveInputProps<string>> {
|
||||
className={`${this.props.value.invalid ? 'bg-danger' : this.props.value.touched ? 'bg-success' : ''} bg-opacity-10`}
|
||||
onChange={this.onChange} value={this.props.value.value}/>
|
||||
</FloatingLabel>
|
||||
<Button onClick={this.toggleShowPassword} variant={"outline-secondary"}>
|
||||
<Button onClick={this.toggleShowPassword} variant={"outline-secondary"} className={'ms-2'}>
|
||||
<FontAwesomeIcon icon={this.showPassword ? 'eye-slash' : 'eye'}/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
55
web/src/components/group/CreateGroupModal.tsx
Normal file
55
web/src/components/group/CreateGroupModal.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import {ComponentContext} from "../../utils/ComponentContext";
|
||||
import {observer} from "mobx-react";
|
||||
import {ModalState} from "../../utils/modalState";
|
||||
import {action, computed, makeObservable, observable} from "mobx";
|
||||
import {ReactiveValue} from "../../utils/reactive/reactiveValue";
|
||||
import {required, strLength, strPattern} from "../../utils/reactive/validators";
|
||||
import {Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle} from "react-bootstrap";
|
||||
import {StringInput} from "../custom/controls/ReactiveControls";
|
||||
import {post} from "../../utils/request";
|
||||
|
||||
export interface CreateGroupModalProps {
|
||||
modalState: ModalState;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class CreateGroupModal extends ComponentContext<CreateGroupModalProps> {
|
||||
@observable name = new ReactiveValue<string>()
|
||||
.addValidator(required)
|
||||
.addValidator(strLength(3, 50))
|
||||
.addValidator(strPattern(/^[а-яА-ЯёЁ0-9_-]*$/, "Имя группы должно содержать только русские буквы, дефис, нижнее подчеркивание и цифры"));
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
creationRequest() {
|
||||
post<void>('/group/create-group', {name: this.name.value}).then(() => {
|
||||
this.notificationStore.success(`Группа ${this.name.value} создана`);
|
||||
}).finally(() => {
|
||||
this.props.modalState.close();
|
||||
});
|
||||
}
|
||||
|
||||
@computed
|
||||
get formInvalid() {
|
||||
return this.name.invalid || !this.name.touched;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Modal show={this.props.modalState.isOpen}>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Создание группы</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<StringInput value={this.name} label={'Имя группы'}/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={this.creationRequest} disabled={this.formInvalid}>Создать</Button>
|
||||
<Button onClick={this.props.modalState.close}>Закрыть</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
@ -6,14 +6,18 @@ import {observer} from "mobx-react";
|
||||
import {post} from "../../utils/request";
|
||||
import {LoginModal} from "../user/LoginModal";
|
||||
import {ModalState} from "../../utils/modalState";
|
||||
import {action, makeObservable} from "mobx";
|
||||
import {action, makeObservable, observable} from "mobx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {ComponentContext} from "../../utils/ComponentContext";
|
||||
import {CreateGroupModal} from "../group/CreateGroupModal";
|
||||
import {UserRegistrationModal} from "../user/UserRegistrationModal";
|
||||
|
||||
@observer
|
||||
class Header extends ComponentContext {
|
||||
|
||||
loginModalState = new ModalState();
|
||||
@observable loginModalState = new ModalState();
|
||||
@observable createGroupModalState = new ModalState();
|
||||
@observable userRegistrationModalState = new ModalState();
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
@ -37,10 +41,17 @@ class Header extends ComponentContext {
|
||||
user.authenticated && userStore.isAdministrator() &&
|
||||
<NavDropdown title="Пользователи">
|
||||
<NavDropdown.Item as={RouterLink} routeName={'userList'} children={'Список'}/>
|
||||
<NavDropdown.Item as={RouterLink} routeName={'userRegistration'}
|
||||
<NavDropdown.Item onClick={this.userRegistrationModalState.open}
|
||||
children={'Зарегистрировать'}/>
|
||||
</NavDropdown>
|
||||
}
|
||||
{
|
||||
user.authenticated && userStore.isAdministrator() &&
|
||||
<NavDropdown title="Группы">
|
||||
<NavDropdown.Item as={RouterLink} routeName={'groupList'} children={'Список'}/>
|
||||
<NavDropdown.Item onClick={this.createGroupModalState.open} children={'Добавить'}/>
|
||||
</NavDropdown>
|
||||
}
|
||||
</Nav>
|
||||
|
||||
<Nav className="ms-auto">
|
||||
@ -63,6 +74,8 @@ class Header extends ComponentContext {
|
||||
</Navbar>
|
||||
</header>
|
||||
<LoginModal modalState={this.loginModalState}/>
|
||||
<CreateGroupModal modalState={this.createGroupModalState}/>
|
||||
<UserRegistrationModal modalState={this.userRegistrationModalState}/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import {observer} from "mobx-react";
|
||||
import {DefaultPage} from "../layout/DefaultPage";
|
||||
import {action, computed, makeObservable, observable} from "mobx";
|
||||
import {Button, Col, Form, Row} from "react-bootstrap";
|
||||
import {Button, Col, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle, Row} from "react-bootstrap";
|
||||
import {UserRegistrationDTO} from "../../models/registration";
|
||||
import {post} from "../../utils/request";
|
||||
import {ReactiveValue} from "../../utils/reactive/reactiveValue";
|
||||
@ -17,9 +16,15 @@ import {
|
||||
phone,
|
||||
required
|
||||
} from "../../utils/reactive/validators";
|
||||
import {ComponentContext} from "../../utils/ComponentContext";
|
||||
import {ModalState} from "../../utils/modalState";
|
||||
|
||||
export interface UserRegistrationModalProps {
|
||||
modalState: ModalState;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class UserRegistration extends DefaultPage {
|
||||
export class UserRegistrationModal extends ComponentContext<UserRegistrationModalProps> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
@ -31,7 +36,6 @@ export class UserRegistration extends DefaultPage {
|
||||
@observable email = new ReactiveValue<string>().addValidator(required).addValidator(email);
|
||||
@observable numberPhone = new ReactiveValue<string>().addValidator(required).addValidator(phone).setAuto('+7');
|
||||
|
||||
|
||||
@observable accountType = new ReactiveValue<string>().addValidator(required).addValidator((value) => {
|
||||
if (!['student', 'admin'].includes(value)) {
|
||||
return 'Тип аккаунта должен быть "СТУДЕНТ" или "АДМИНИСТРАТОР"';
|
||||
@ -64,9 +68,13 @@ export class UserRegistration extends DefaultPage {
|
||||
});
|
||||
}
|
||||
|
||||
get page() {
|
||||
return <div className={'w-75 ms-auto me-auto'}>
|
||||
<Form>
|
||||
render() {
|
||||
return <Modal show={this.props.modalState.isOpen} size={'lg'}>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Регистрация пользователя</ModalTitle>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Row>
|
||||
<Col>
|
||||
<StringInput value={this.login} label={"Логин"}/>
|
||||
@ -78,10 +86,12 @@ export class UserRegistration extends DefaultPage {
|
||||
<StringInput value={this.numberPhone} label={"Телефон"}/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<SelectButtonInput value={this.accountType} label={'Тип аккаунта'}/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button disabled={this.formInvalid} onClick={this.submit}>Зарегистрировать</Button>
|
||||
</Form>
|
||||
</div>
|
||||
<Button onClick={this.props.modalState.close} variant={'secondary'}>Закрыть</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
12
web/src/models/error.ts
Normal file
12
web/src/models/error.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export enum ErrorCode {
|
||||
BUSINESS_ERROR = 'BUSINESS_ERROR',
|
||||
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||
INTERNAL_ERROR = 'INTERNAL_ERROR',
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
ACCESS_DENIED = 'ACCESS_DENIED',
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
message: string;
|
||||
errorCode: ErrorCode;
|
||||
}
|
||||
@ -9,9 +9,6 @@ export const routes: Route[] = [{
|
||||
}, {
|
||||
name: 'userList',
|
||||
pattern: '/users',
|
||||
}, {
|
||||
name: 'userRegistration',
|
||||
pattern: '/user-registration',
|
||||
}, {
|
||||
name: 'error',
|
||||
pattern: '/error',
|
||||
|
||||
@ -3,12 +3,10 @@ import Home from "../components/layout/Home";
|
||||
import Error from "../components/layout/Error";
|
||||
import UserProfilePage from "../components/user/UserProfilePage";
|
||||
import {UserList} from "../components/user/UserList";
|
||||
import {UserRegistration} from "../components/user/UserRegistration";
|
||||
|
||||
export const viewMap: ViewMap = {
|
||||
root: <Home/>,
|
||||
profile: <UserProfilePage/>,
|
||||
userList: <UserList/>,
|
||||
userRegistration: <UserRegistration/>,
|
||||
error: <Error/>,
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
import {action, computed, makeObservable, observable, reaction} from "mobx";
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
|
||||
|
||||
export class ReactiveValue<T> {
|
||||
@ -95,33 +93,33 @@ export class ReactiveValue<T> {
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberField extends ReactiveValue<number> {
|
||||
constructor(fireImmediately: boolean = false) {
|
||||
super(fireImmediately);
|
||||
makeObservable(this);
|
||||
this.addValidator(value => {
|
||||
if (_.isNaN(value)) {
|
||||
return 'Должно быть числом';
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.set(_.toNumber(event.currentTarget.value));
|
||||
}
|
||||
}
|
||||
|
||||
export class BooleanField extends ReactiveValue<boolean> {
|
||||
constructor(fireImmediately: boolean = false) {
|
||||
super(fireImmediately);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.set(event.currentTarget.checked);
|
||||
}
|
||||
}
|
||||
// export class NumberField extends ReactiveValue<number> {
|
||||
// constructor(fireImmediately: boolean = false) {
|
||||
// super(fireImmediately);
|
||||
// makeObservable(this);
|
||||
// this.addValidator(value => {
|
||||
// if (_.isNaN(value)) {
|
||||
// return 'Должно быть числом';
|
||||
// }
|
||||
//
|
||||
// return null;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// @action.bound
|
||||
// onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
// this.set(_.toNumber(event.currentTarget.value));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// export class BooleanField extends ReactiveValue<boolean> {
|
||||
// constructor(fireImmediately: boolean = false) {
|
||||
// super(fireImmediately);
|
||||
// makeObservable(this);
|
||||
// }
|
||||
//
|
||||
// @action.bound
|
||||
// onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
// this.set(event.currentTarget.checked);
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -3,17 +3,6 @@ export const required = (value: any, field = 'Поле') => {
|
||||
return `${field} обязательно для заполнения`;
|
||||
}
|
||||
}
|
||||
export const greaterThan = (min: number) => (value: number, field = 'Значение') => {
|
||||
if (value && value <= min) {
|
||||
return `${field} должно быть больше ${min}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const lessThan = (max: number) => (value: number, field = 'Значение') => {
|
||||
if (value && value >= max) {
|
||||
return `${field} должно быть меньше ${max}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const equals = (expected: any) => (value: any, field = 'Поле') => {
|
||||
if (value && value !== expected) {
|
||||
@ -21,13 +10,31 @@ export const equals = (expected: any) => (value: any, field = 'Поле') => {
|
||||
}
|
||||
}
|
||||
|
||||
export const length = (min: number, max: number) => (value: string, field = 'Поле') => {
|
||||
export const number = (value: string, field = 'Поле') => {
|
||||
if (!/^\d+$/.test(value)) {
|
||||
return `${field} должно быть числом`;
|
||||
}
|
||||
}
|
||||
|
||||
export const numGreaterThan = (min: number) => (value: number, field = 'Значение') => {
|
||||
if (value && value <= min) {
|
||||
return `${field} должно быть больше ${min}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const numLessThan = (max: number) => (value: number, field = 'Значение') => {
|
||||
if (value && value >= max) {
|
||||
return `${field} должно быть меньше ${max}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const strLength = (min: number, max: number) => (value: string, field = 'Поле') => {
|
||||
if (value && (value.length < min || value.length > max)) {
|
||||
return `${field} должно содержать от ${min} до ${max} символов`;
|
||||
}
|
||||
}
|
||||
|
||||
export const pattern = (regexp: RegExp, message: string) => (value: string, field = '') => {
|
||||
export const strPattern = (regexp: RegExp, message: string) => (value: string, field = '') => {
|
||||
if (!regexp.test(value)) {
|
||||
return message;
|
||||
}
|
||||
@ -81,12 +88,6 @@ export const nameLength = (value: string, field = 'Поле') => {
|
||||
}
|
||||
}
|
||||
|
||||
export const number = (value: string, field = 'Поле') => {
|
||||
if (!/^\d+$/.test(value)) {
|
||||
return `${field} должно быть числом`;
|
||||
}
|
||||
}
|
||||
|
||||
export const date = (value: string, field = 'Поле') => {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
return `${field} должно быть датой в формате ГГГГ-ММ-ДД`;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import axios, {AxiosError, AxiosRequestConfig} from "axios";
|
||||
import {NotificationService} from "../services/NotificationService";
|
||||
import {ErrorResponse} from "../models/error";
|
||||
|
||||
export const apiUrl = "http://localhost:8080/api/v1/";
|
||||
|
||||
@ -21,7 +22,7 @@ export const request = async <R>(config: AxiosRequestConfig<any>, doReject: bool
|
||||
resolve(response.data);
|
||||
}).catch((error: AxiosError) => {
|
||||
if (showError) {
|
||||
if (error.response) NotificationService.error(JSON.stringify(error.response.data), 'Сервер вернул ошибку, код статуса: ' + error.response.status);
|
||||
if (error.response) NotificationService.error((error.response.data as ErrorResponse).message, 'Сервер вернул ошибку, код статуса: ' + error.response.status);
|
||||
else if (error.request) NotificationService.error(error.request, 'Не удалось получить ответ от сервера');
|
||||
else NotificationService.error(error.message, 'Запрос отправить не удалось');
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user