From 83ae03de961b53f4dbc73b2c363ccf753c15bf85 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 13 Jul 2025 21:44:21 +0700 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=BD=D0=BE=D0=B2=D1=83=D1=8E=20?= =?UTF-8?q?=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20-=20"=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B8"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../ru/practicum/evmsevice/model/Comment.java | 30 +++++++++++++++++++ .../repository/CommentRepository.java | 7 +++++ ewm-service/src/main/resources/schema.sql | 13 +++++++- 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java diff --git a/README.md b/README.md index d36813d..a6ee1ef 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # java-explore-with-me -Итоговый проект учебного курса. май 2025. гр. 53. +Итоговый проект учебного курса. май 2025. diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java new file mode 100644 index 0000000..3282eb5 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java @@ -0,0 +1,30 @@ +package ru.practicum.evmsevice.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Entity +@Setter +@Getter +@Table(name = "comments", schema = "public") +@NoArgsConstructor +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @ManyToOne + @JoinColumn(name = "author_id", nullable = false) + private User author; + @Column(name = "event_id", nullable = false) + private Integer eventId; + @Column(name = "text", nullable = false) + private String text; + @Column(name = "comment_time", nullable = false) + private LocalDateTime commentTime; + @Column(name = "edit_time") + private LocalDateTime editTime; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java new file mode 100644 index 0000000..430f1a0 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.evmsevice.model.Comment; + +public interface CommentRepository extends JpaRepository { +} diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index 8c871d8..6790a16 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -59,7 +59,6 @@ CREATE TABLE IF NOT EXISTS compilations CONSTRAINT UQ_COMPILATION_TITLE UNIQUE (title) ); - CREATE TABLE IF NOT EXISTS eventlinks ( event_id INTEGER NOT NULL, @@ -68,3 +67,15 @@ CREATE TABLE IF NOT EXISTS eventlinks CONSTRAINT fk_links_to_events FOREIGN KEY (event_id) REFERENCES events (id), CONSTRAINT fk_links_to_compilations FOREIGN KEY (compilation_id) REFERENCES compilations (id) ); + +CREATE TABLE IF NOT EXISTS comments +( + id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + author_id INTEGER NOT NULL, + event_id INTEGER NOT NULL, + text VARCHAR(2000) NOT NULL, + comment_time TIMESTAMP WITHOUT TIME ZONE, + edit_time TIMESTAMP WITHOUT TIME ZONE, + CONSTRAINT pk_comment PRIMARY KEY (id), + CONSTRAINT fk_event_for_comment FOREIGN KEY (event_id) REFERENCES events (id) +); \ No newline at end of file From 819f854d7d428f8b2f20532628628a2dff15d42a Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Mon, 14 Jul 2025 22:34:14 +0700 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5,=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=D1=80=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evmsevice/controller/UserController.java | 22 ++++++ .../practicum/evmsevice/dto/CommentDto.java | 26 +++++++ .../evmsevice/dto/NewCommentDto.java | 18 +++++ .../evmsevice/mapper/CommentMapper.java | 30 ++++++++ .../evmsevice/service/CommentService.java | 14 ++++ .../evmsevice/service/CommentServiceImpl.java | 73 +++++++++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index 29f8748..b5d603b 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -8,6 +8,7 @@ import ru.practicum.evmsevice.dto.*; import ru.practicum.evmsevice.mapper.RequestMapper; import ru.practicum.evmsevice.model.Request; +import ru.practicum.evmsevice.service.CommentService; import ru.practicum.evmsevice.service.EventService; import ru.practicum.evmsevice.service.RequestService; @@ -20,6 +21,7 @@ public class UserController { private final EventService eventService; private final RequestService requestService; + private final CommentService commentService; @PostMapping("/{id}/events") @ResponseStatus(HttpStatus.CREATED) @@ -103,4 +105,24 @@ public RequestDto canceledRequestById(@PathVariable Integer userId, log.info("Пользователь id={} отменяет запрос id={}.", userId, requestId); return RequestMapper.toRequestDto(requestService.canceledRequest(userId, requestId)); } + + @PostMapping("/{userId}/events/{eventId}/comments") + @ResponseStatus(HttpStatus.CREATED) + public CommentDto addNewComment(@PathVariable Integer userId, + @PathVariable Integer eventId, + @RequestBody @Validated NewCommentDto commentDto) { + log.info("Пользователь id={} добавляет комментарий к событию id={}. {}", + userId, eventId, commentDto.toString()); + return commentService.addComment(userId, eventId, commentDto); + } + + @PatchMapping("/{userId}/comments/{commentId}") + @ResponseStatus(HttpStatus.OK) + public CommentDto updateComment(@PathVariable Integer userId, + @PathVariable Integer commentId, + @RequestBody @Validated NewCommentDto commentDto) { + log.info("Пользователь id={} редактирует комментарий id={}. {}", + userId, commentId, commentDto.toString()); + return commentService.updateComment(userId, commentId, commentDto); + } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java new file mode 100644 index 0000000..ac7827a --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java @@ -0,0 +1,26 @@ +package ru.practicum.evmsevice.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CommentDto { + private Integer id; + private UserDto author; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime commentTime; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime editTime; + private Integer eventId; + private String text; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java new file mode 100644 index 0000000..60414d1 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java @@ -0,0 +1,18 @@ +package ru.practicum.evmsevice.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NewCommentDto { + @NotBlank(message = "Текст комментария должен быть задан.") + @Size(min = 3, max = 2000, message = "Длина комментария должна быть от 3 до 2000 символов.") + private String text; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java new file mode 100644 index 0000000..78d0710 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java @@ -0,0 +1,30 @@ +package ru.practicum.evmsevice.mapper; + +import ru.practicum.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.model.Comment; + +import java.time.LocalDateTime; + +public class CommentMapper { + private CommentMapper() { + } + + public static Comment getComment(NewCommentDto newCommemtDto) { + Comment comment = new Comment(); + comment.setCommentTime(LocalDateTime.now()); + comment.setText(newCommemtDto.getText()); + return comment; + } + + public static CommentDto toDto(Comment comment) { + CommentDto commentDto = new CommentDto(); + commentDto.setId(comment.getId()); + commentDto.setEventId(comment.getEventId()); + commentDto.setText(comment.getText()); + commentDto.setCommentTime(comment.getCommentTime()); + commentDto.setEditTime(comment.getEditTime()); + commentDto.setAuthor(UserMapper.toUserDto(comment.getAuthor())); + return commentDto; + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java new file mode 100644 index 0000000..9726ec3 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java @@ -0,0 +1,14 @@ +package ru.practicum.evmsevice.service; + +import ru.practicum.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.NewCommentDto; + +import java.util.List; + +public interface CommentService { + CommentDto addComment(Integer userId, Integer eventId, NewCommentDto commentDto); + CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto commentDto); + CommentDto getCommentById(Integer commentId); + List getCommentsByEventId(Integer eventId); + List getCommentsByUserId(Integer userId); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java new file mode 100644 index 0000000..214c942 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java @@ -0,0 +1,73 @@ +package ru.practicum.evmsevice.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.exception.NotFoundException; +import ru.practicum.evmsevice.exception.ValidationException; +import ru.practicum.evmsevice.mapper.CommentMapper; +import ru.practicum.evmsevice.model.Comment; +import ru.practicum.evmsevice.model.Event; +import ru.practicum.evmsevice.model.User; +import ru.practicum.evmsevice.repository.CommentRepository; +import ru.practicum.evmsevice.repository.EventRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CommentServiceImpl implements CommentService { + private final UserService userService; + private final EventRepository eventRepository; + private final CommentRepository commentRepository; + + @Override + @Transactional + public CommentDto addComment(Integer userId, Integer eventId, NewCommentDto commentDto) { + User user = userService.getUserById(userId); + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new NotFoundException("Не надено событие id=" + eventId)); + Comment comment = CommentMapper.getComment(commentDto); + comment.setAuthor(user); + comment.setEventId(eventId); + comment.setCommentTime(LocalDateTime.now()); + Comment savedComment = commentRepository.save(comment); + return CommentMapper.toDto(savedComment); + } + + @Override + @Transactional + public CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto commentDto) { + User user = userService.getUserById(userId); + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException("Не найден комментарий id=" + commentId)); + if (!comment.getAuthor().getId().equals(user.getId())) { + throw new ValidationException("Редактировать комментарий может только его автор."); + } + comment.setText(commentDto.getText()); + comment.setEditTime(LocalDateTime.now()); + Comment savedComment = commentRepository.save(comment); + return CommentMapper.toDto(savedComment); + } + + @Override + public CommentDto getCommentById(Integer commentId) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException("Не найден комментарий id=" + commentId)); + return CommentMapper.toDto(comment); + } + + @Override + public List getCommentsByEventId(Integer eventId) { + return List.of(); + } + + @Override + public List getCommentsByUserId(Integer userId) { + return List.of(); + } +} From 4e32da1fa1156f9f05e17e4aa01361ac1f36e369 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 15 Jul 2025 22:46:59 +0700 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=20=D0=BA=D0=BE=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D0=B8=D0=B5=D0=B2=20CommentController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CommentController.java | 70 +++++++++++++++++++ .../evmsevice/controller/UserController.java | 18 ----- .../practicum/evmsevice/dto/CommentDto.java | 4 +- .../evmsevice/mapper/CommentMapper.java | 6 +- .../ru/practicum/evmsevice/model/Comment.java | 8 +-- .../repository/CommentRepository.java | 4 ++ .../evmsevice/service/CommentService.java | 1 + .../evmsevice/service/CommentServiceImpl.java | 29 ++++++-- 8 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java new file mode 100644 index 0000000..6f68df3 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java @@ -0,0 +1,70 @@ +package ru.practicum.evmsevice.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.service.CommentService; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/comments") +public class CommentController { + private final CommentService commentService; + + @PostMapping("/users/{userId}/events/{eventId}") + @ResponseStatus(HttpStatus.CREATED) + public CommentDto addNewComment(@PathVariable Integer userId, + @PathVariable Integer eventId, + @RequestBody @Validated NewCommentDto commentDto) { + log.info("Пользователь id={} добавляет комментарий к событию id={}. {}", + userId, eventId, commentDto.toString()); + return commentService.addComment(userId, eventId, commentDto); + } + + @PatchMapping("/users/{userId}/patch/{commentId}") + @ResponseStatus(HttpStatus.OK) + public CommentDto updateComment(@PathVariable Integer userId, + @PathVariable Integer commentId, + @RequestBody @Validated NewCommentDto commentDto) { + log.info("Пользователь id={} редактирует комментарий id={}. {}", + userId, commentId, commentDto.toString()); + return commentService.updateComment(userId, commentId, commentDto); + } + + @GetMapping("/users/{userId}") + @ResponseStatus(HttpStatus.OK) + public List getCommentsByUserId(@PathVariable Integer userId) { + log.info("Поиск всех коментариев пользователя id={}.", userId); + return commentService.getCommentsByUserId(userId); + } + + @GetMapping("/events/{eventId}") + @ResponseStatus(HttpStatus.OK) + public List getCommentsByEventId(@PathVariable Integer eventId) { + log.info("Поиск всех коментариев к событию id={}.", eventId); + return commentService.getCommentsByEventId(eventId); + } + + @GetMapping("/{commentId}") + @ResponseStatus(HttpStatus.OK) + public CommentDto getComment(@PathVariable Integer commentId) { + log.info("Поиск коментария id={}.", commentId); + return commentService.getCommentById(commentId); + } + + @DeleteMapping("/{commentId}/users/{userId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteComment(@PathVariable Integer commentId, + @PathVariable Integer userId) { + + log.info("Пользователь id={} удаляет комментарий id={}.", userId, commentId); + commentService.deleteComment(userId, commentId); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java index b5d603b..d83245a 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserController.java @@ -106,23 +106,5 @@ public RequestDto canceledRequestById(@PathVariable Integer userId, return RequestMapper.toRequestDto(requestService.canceledRequest(userId, requestId)); } - @PostMapping("/{userId}/events/{eventId}/comments") - @ResponseStatus(HttpStatus.CREATED) - public CommentDto addNewComment(@PathVariable Integer userId, - @PathVariable Integer eventId, - @RequestBody @Validated NewCommentDto commentDto) { - log.info("Пользователь id={} добавляет комментарий к событию id={}. {}", - userId, eventId, commentDto.toString()); - return commentService.addComment(userId, eventId, commentDto); - } - @PatchMapping("/{userId}/comments/{commentId}") - @ResponseStatus(HttpStatus.OK) - public CommentDto updateComment(@PathVariable Integer userId, - @PathVariable Integer commentId, - @RequestBody @Validated NewCommentDto commentDto) { - log.info("Пользователь id={} редактирует комментарий id={}. {}", - userId, commentId, commentDto.toString()); - return commentService.updateComment(userId, commentId, commentDto); - } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java index ac7827a..4d43fdc 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java @@ -18,9 +18,9 @@ public class CommentDto { private Integer id; private UserDto author; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private LocalDateTime commentTime; + private LocalDateTime createdOn; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private LocalDateTime editTime; + private LocalDateTime editedOn; private Integer eventId; private String text; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java index 78d0710..a7a3648 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java @@ -12,7 +12,7 @@ private CommentMapper() { public static Comment getComment(NewCommentDto newCommemtDto) { Comment comment = new Comment(); - comment.setCommentTime(LocalDateTime.now()); + comment.setCreatedOn(LocalDateTime.now()); comment.setText(newCommemtDto.getText()); return comment; } @@ -22,8 +22,8 @@ public static CommentDto toDto(Comment comment) { commentDto.setId(comment.getId()); commentDto.setEventId(comment.getEventId()); commentDto.setText(comment.getText()); - commentDto.setCommentTime(comment.getCommentTime()); - commentDto.setEditTime(comment.getEditTime()); + commentDto.setCreatedOn(comment.getCreatedOn()); + commentDto.setEditedOn(comment.getEditedOn()); commentDto.setAuthor(UserMapper.toUserDto(comment.getAuthor())); return commentDto; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java index 3282eb5..b652b1a 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java @@ -23,8 +23,8 @@ public class Comment { private Integer eventId; @Column(name = "text", nullable = false) private String text; - @Column(name = "comment_time", nullable = false) - private LocalDateTime commentTime; - @Column(name = "edit_time") - private LocalDateTime editTime; + @Column(name = "created_on", nullable = false) + private LocalDateTime createdOn; + @Column(name = "edited_on") + private LocalDateTime editedOn; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java index 430f1a0..97def1b 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java @@ -3,5 +3,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import ru.practicum.evmsevice.model.Comment; +import java.util.List; + public interface CommentRepository extends JpaRepository { + List findAllByEventId(Integer eventId); + List findAllByAuthor_Id(Integer authorId); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java index 9726ec3..8357e22 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java @@ -11,4 +11,5 @@ public interface CommentService { CommentDto getCommentById(Integer commentId); List getCommentsByEventId(Integer eventId); List getCommentsByUserId(Integer userId); + void deleteComment(Integer userId, Integer commentId); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java index 214c942..98d2f0b 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java @@ -5,6 +5,7 @@ import org.springframework.transaction.annotation.Transactional; import ru.practicum.evmsevice.dto.CommentDto; import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.exception.DataConflictException; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.exception.ValidationException; import ru.practicum.evmsevice.mapper.CommentMapper; @@ -30,11 +31,11 @@ public class CommentServiceImpl implements CommentService { public CommentDto addComment(Integer userId, Integer eventId, NewCommentDto commentDto) { User user = userService.getUserById(userId); Event event = eventRepository.findById(eventId) - .orElseThrow(() -> new NotFoundException("Не надено событие id=" + eventId)); + .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); Comment comment = CommentMapper.getComment(commentDto); comment.setAuthor(user); comment.setEventId(eventId); - comment.setCommentTime(LocalDateTime.now()); + comment.setCreatedOn(LocalDateTime.now()); Comment savedComment = commentRepository.save(comment); return CommentMapper.toDto(savedComment); } @@ -49,7 +50,7 @@ public CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto throw new ValidationException("Редактировать комментарий может только его автор."); } comment.setText(commentDto.getText()); - comment.setEditTime(LocalDateTime.now()); + comment.setEditedOn(LocalDateTime.now()); Comment savedComment = commentRepository.save(comment); return CommentMapper.toDto(savedComment); } @@ -63,11 +64,29 @@ public CommentDto getCommentById(Integer commentId) { @Override public List getCommentsByEventId(Integer eventId) { - return List.of(); + return commentRepository.findAllByEventId(eventId).stream() + .map(CommentMapper::toDto) + .toList(); } @Override public List getCommentsByUserId(Integer userId) { - return List.of(); + return commentRepository.findAllByAuthor_Id(userId).stream() + .map(CommentMapper::toDto) + .toList(); + } + + @Override + @Transactional + public void deleteComment(Integer userId, Integer commentId) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException("Не найден комментарий id=" + commentId)); + if (!comment.getAuthor().getId().equals(userId)) { + throw new DataConflictException(String.format( + "Пользователь id=%d не является автором комментария id=%d.", + userId, commentId + )); + } + commentRepository.delete(comment); } } From 419fbdb4ed1f0ae7d3acc34a826f43aae25de1fa Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 16 Jul 2025 22:10:08 +0700 Subject: [PATCH 04/10] fix: CommentController --- .../evmsevice/controller/CommentController.java | 12 ++++++++---- .../practicum/evmsevice/service/CommentService.java | 4 ++-- .../evmsevice/service/CommentServiceImpl.java | 8 ++++++-- ewm-service/src/main/resources/schema.sql | 6 +++--- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java index 6f68df3..ea2505e 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java @@ -40,16 +40,20 @@ public CommentDto updateComment(@PathVariable Integer userId, @GetMapping("/users/{userId}") @ResponseStatus(HttpStatus.OK) - public List getCommentsByUserId(@PathVariable Integer userId) { + public List getCommentsByUserId(@PathVariable Integer userId, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { log.info("Поиск всех коментариев пользователя id={}.", userId); - return commentService.getCommentsByUserId(userId); + return commentService.getCommentsByUserId(userId, from, size); } @GetMapping("/events/{eventId}") @ResponseStatus(HttpStatus.OK) - public List getCommentsByEventId(@PathVariable Integer eventId) { + public List getCommentsByEventId(@PathVariable Integer eventId, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { log.info("Поиск всех коментариев к событию id={}.", eventId); - return commentService.getCommentsByEventId(eventId); + return commentService.getCommentsByEventId(eventId, from, size); } @GetMapping("/{commentId}") diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java index 8357e22..62c63ff 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java @@ -9,7 +9,7 @@ public interface CommentService { CommentDto addComment(Integer userId, Integer eventId, NewCommentDto commentDto); CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto commentDto); CommentDto getCommentById(Integer commentId); - List getCommentsByEventId(Integer eventId); - List getCommentsByUserId(Integer userId); + List getCommentsByEventId(Integer eventId, Integer from, Integer ize); + List getCommentsByUserId(Integer userId, Integer from, Integer ize); void deleteComment(Integer userId, Integer commentId); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java index 98d2f0b..99b6fd1 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java @@ -63,16 +63,20 @@ public CommentDto getCommentById(Integer commentId) { } @Override - public List getCommentsByEventId(Integer eventId) { + public List getCommentsByEventId(Integer eventId, Integer from, Integer ize) { return commentRepository.findAllByEventId(eventId).stream() .map(CommentMapper::toDto) + .skip(from) + .limit(ize) .toList(); } @Override - public List getCommentsByUserId(Integer userId) { + public List getCommentsByUserId(Integer userId, Integer from, Integer ize) { return commentRepository.findAllByAuthor_Id(userId).stream() .map(CommentMapper::toDto) + .skip(from) + .limit(ize) .toList(); } diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index 6790a16..d470393 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -74,8 +74,8 @@ CREATE TABLE IF NOT EXISTS comments author_id INTEGER NOT NULL, event_id INTEGER NOT NULL, text VARCHAR(2000) NOT NULL, - comment_time TIMESTAMP WITHOUT TIME ZONE, - edit_time TIMESTAMP WITHOUT TIME ZONE, + created_on TIMESTAMP WITHOUT TIME ZONE, + edited_on TIMESTAMP WITHOUT TIME ZONE, CONSTRAINT pk_comment PRIMARY KEY (id), CONSTRAINT fk_event_for_comment FOREIGN KEY (event_id) REFERENCES events (id) -); \ No newline at end of file +);); \ No newline at end of file From 69e9e008b9d584468afef2bd475f14bb38f45cc7 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 17 Jul 2025 22:40:56 +0700 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BC=D0=BE=D0=B4=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8?= =?UTF-8?q?=D0=B5=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PublicController.java | 24 ++++++++-- ...roller.java => UserCommentController.java} | 40 ++++++++-------- .../practicum/evmsevice/dto/CommentDto.java | 2 + .../evmsevice/dto/CommentModerationDto.java | 19 ++++++++ .../evmsevice/dto/CommentsGroupDto.java | 18 ++++++++ .../evmsevice/enums/CommentState.java | 18 ++++++++ .../evmsevice/mapper/CommentMapper.java | 1 + .../ru/practicum/evmsevice/model/Comment.java | 5 ++ .../evmsevice/service/CommentService.java | 3 ++ .../evmsevice/service/CommentServiceImpl.java | 46 +++++++++++++++++++ ewm-service/src/main/resources/schema.sql | 3 +- 11 files changed, 153 insertions(+), 26 deletions(-) rename ewm-service/src/main/java/ru/practicum/evmsevice/controller/{CommentController.java => UserCommentController.java} (69%) create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentModerationDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/enums/CommentState.java diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index f2d57af..5fd4020 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -7,16 +7,14 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.practicum.evmsevice.client.StatsClient; -import ru.practicum.evmsevice.dto.CategoryDto; -import ru.practicum.evmsevice.dto.CompilationDto; -import ru.practicum.evmsevice.dto.EventFullDto; -import ru.practicum.evmsevice.dto.EventShortDto; +import ru.practicum.evmsevice.dto.*; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.mapper.CategoryMapper; import ru.practicum.evmsevice.mapper.EventMapper; import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.service.CategoryService; +import ru.practicum.evmsevice.service.CommentService; import ru.practicum.evmsevice.service.CompilationService; import ru.practicum.evmsevice.service.EventService; @@ -31,6 +29,7 @@ public class PublicController { private final EventService eventService; private final CompilationService compilationService; private final CategoryService categoryService; + private final CommentService commentService; @Value("${spring.application.name}") private String appName; @@ -108,4 +107,21 @@ public CategoryDto findCategoryById(@PathVariable("catId") int catId) { log.info("Пользователь запрашивает категорию id={}.", catId); return CategoryMapper.toDto(categoryService.getCategoryById(catId)); } + + @GetMapping("/events/{eventId}/comments") + @ResponseStatus(HttpStatus.OK) + public List getCommentsByEventId(@PathVariable Integer eventId, + @RequestParam(name = "from", defaultValue = "0") Integer from, + @RequestParam(name = "size", defaultValue = "10") Integer size) { + log.info("Поиск всех коментариев к событию id={}.", eventId); + return commentService.getCommentsByEventId(eventId, from, size); + } + + @GetMapping("/comments/{commentId}") + @ResponseStatus(HttpStatus.OK) + public CommentDto getComment(@PathVariable Integer commentId) { + log.info("Поиск коментария id={}.", commentId); + return commentService.getCommentById(commentId); + } + } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java similarity index 69% rename from ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java rename to ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java index ea2505e..daa3638 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/CommentController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java @@ -6,6 +6,8 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.practicum.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.CommentModerationDto; +import ru.practicum.evmsevice.dto.CommentsGroupDto; import ru.practicum.evmsevice.dto.NewCommentDto; import ru.practicum.evmsevice.service.CommentService; @@ -14,11 +16,11 @@ @Slf4j @RequiredArgsConstructor @RestController -@RequestMapping("/comments") -public class CommentController { +@RequestMapping("/users") +public class UserCommentController { private final CommentService commentService; - @PostMapping("/users/{userId}/events/{eventId}") + @PostMapping("/{userId}/events/{eventId}/comments") @ResponseStatus(HttpStatus.CREATED) public CommentDto addNewComment(@PathVariable Integer userId, @PathVariable Integer eventId, @@ -28,7 +30,7 @@ public CommentDto addNewComment(@PathVariable Integer userId, return commentService.addComment(userId, eventId, commentDto); } - @PatchMapping("/users/{userId}/patch/{commentId}") + @PatchMapping("/{userId}/comments/{commentId}") @ResponseStatus(HttpStatus.OK) public CommentDto updateComment(@PathVariable Integer userId, @PathVariable Integer commentId, @@ -38,7 +40,18 @@ public CommentDto updateComment(@PathVariable Integer userId, return commentService.updateComment(userId, commentId, commentDto); } - @GetMapping("/users/{userId}") + @PatchMapping("/{userId}/events/{eventId}/comments") + @ResponseStatus(HttpStatus.CREATED) + public CommentsGroupDto addNewComment(@PathVariable Integer userId, + @PathVariable Integer eventId, + @RequestBody CommentModerationDto cmModDto) { + log.info("Пользователь id={}модерирует комментарии к событию id={}. {}", + userId, eventId, cmModDto.toString()); + return commentService.moderationComments(userId, eventId, cmModDto); + } + + + @GetMapping("/{userId}/comments") @ResponseStatus(HttpStatus.OK) public List getCommentsByUserId(@PathVariable Integer userId, @RequestParam(name = "from", defaultValue = "0") Integer from, @@ -47,23 +60,8 @@ public List getCommentsByUserId(@PathVariable Integer userId, return commentService.getCommentsByUserId(userId, from, size); } - @GetMapping("/events/{eventId}") - @ResponseStatus(HttpStatus.OK) - public List getCommentsByEventId(@PathVariable Integer eventId, - @RequestParam(name = "from", defaultValue = "0") Integer from, - @RequestParam(name = "size", defaultValue = "10") Integer size) { - log.info("Поиск всех коментариев к событию id={}.", eventId); - return commentService.getCommentsByEventId(eventId, from, size); - } - - @GetMapping("/{commentId}") - @ResponseStatus(HttpStatus.OK) - public CommentDto getComment(@PathVariable Integer commentId) { - log.info("Поиск коментария id={}.", commentId); - return commentService.getCommentById(commentId); - } - @DeleteMapping("/{commentId}/users/{userId}") + @DeleteMapping("/{userId}/comment/{commentId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteComment(@PathVariable Integer commentId, @PathVariable Integer userId) { diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java index 4d43fdc..8d95ae2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java @@ -7,6 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import ru.practicum.evmsevice.enums.CommentState; import java.time.LocalDateTime; @@ -23,4 +24,5 @@ public class CommentDto { private LocalDateTime editedOn; private Integer eventId; private String text; + private CommentState state; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentModerationDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentModerationDto.java new file mode 100644 index 0000000..32b12ff --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentModerationDto.java @@ -0,0 +1,19 @@ +package ru.practicum.evmsevice.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import ru.practicum.evmsevice.enums.CommentState; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@ToString +public class CommentModerationDto { + private List commentIds = new ArrayList<>(); + private CommentState state; +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java new file mode 100644 index 0000000..1e237e2 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java @@ -0,0 +1,18 @@ +package ru.practicum.evmsevice.dto; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@Getter +@NoArgsConstructor +@ToString + +public class CommentsGroupDto { + private List approvedComments = new ArrayList<>(); + private List rejectedComments = new ArrayList<>(); +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/enums/CommentState.java b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/CommentState.java new file mode 100644 index 0000000..222f5b8 --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/enums/CommentState.java @@ -0,0 +1,18 @@ +package ru.practicum.evmsevice.enums; + +import java.util.Optional; + +public enum CommentState { + PENDING, + APPROVED, + REJECTED; + + public static Optional from(String state) { + for (CommentState commentState : CommentState.values()) { + if (commentState.name().equalsIgnoreCase(state)) { + return Optional.of(commentState); + } + } + return Optional.empty(); + } +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java index a7a3648..96e94b3 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/mapper/CommentMapper.java @@ -25,6 +25,7 @@ public static CommentDto toDto(Comment comment) { commentDto.setCreatedOn(comment.getCreatedOn()); commentDto.setEditedOn(comment.getEditedOn()); commentDto.setAuthor(UserMapper.toUserDto(comment.getAuthor())); + commentDto.setState(comment.getState()); return commentDto; } } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java index b652b1a..50982fd 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java @@ -4,6 +4,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import ru.practicum.evmsevice.enums.CommentState; +import ru.practicum.evmsevice.enums.EventState; import java.time.LocalDateTime; @@ -27,4 +29,7 @@ public class Comment { private LocalDateTime createdOn; @Column(name = "edited_on") private LocalDateTime editedOn; + @Column(name = "state") + @Enumerated(EnumType.STRING) + private CommentState state; } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java index 62c63ff..0dacb1d 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java @@ -1,6 +1,8 @@ package ru.practicum.evmsevice.service; import ru.practicum.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.CommentModerationDto; +import ru.practicum.evmsevice.dto.CommentsGroupDto; import ru.practicum.evmsevice.dto.NewCommentDto; import java.util.List; @@ -8,6 +10,7 @@ public interface CommentService { CommentDto addComment(Integer userId, Integer eventId, NewCommentDto commentDto); CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto commentDto); + CommentsGroupDto moderationComments(Integer userId, Integer eventId, CommentModerationDto commentModerationDto); CommentDto getCommentById(Integer commentId); List getCommentsByEventId(Integer eventId, Integer from, Integer ize); List getCommentsByUserId(Integer userId, Integer from, Integer ize); diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java index 99b6fd1..09b0d04 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java @@ -4,7 +4,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.evmsevice.dto.CommentDto; +import ru.practicum.evmsevice.dto.CommentModerationDto; +import ru.practicum.evmsevice.dto.CommentsGroupDto; import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.enums.CommentState; import ru.practicum.evmsevice.exception.DataConflictException; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.exception.ValidationException; @@ -35,6 +38,7 @@ public CommentDto addComment(Integer userId, Integer eventId, NewCommentDto comm Comment comment = CommentMapper.getComment(commentDto); comment.setAuthor(user); comment.setEventId(eventId); + comment.setState(CommentState.PENDING); comment.setCreatedOn(LocalDateTime.now()); Comment savedComment = commentRepository.save(comment); return CommentMapper.toDto(savedComment); @@ -55,6 +59,48 @@ public CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto return CommentMapper.toDto(savedComment); } + /** + * Модерация комментариев к событию + * @param userId - идентификатор инициатора события + * @param eventId - идентификатор события + * @param commentModerationDto - объект идентификаторов коментариев для модерации + * @return - список комментариев с измененным статусом + */ + @Override + @Transactional + public CommentsGroupDto moderationComments(Integer userId, Integer eventId, CommentModerationDto commentModerationDto) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); + if (!event.getInitiator().getId().equals(userId)) { + throw new DataConflictException(String.format( + "Польдователь id=%d не является инициатором события id=%d.", + userId, eventId)); + } + List comments = commentRepository.findAllById(commentModerationDto.getCommentIds()); + CommentsGroupDto cgDto = new CommentsGroupDto(); + CommentState commentState = commentModerationDto.getState(); + for (Comment comment : comments) { + if (!comment.getEventId().equals(eventId)) { + throw new DataConflictException(String.format( + "Комментарий id=%d не относится к событию id=%d.", + comment.getId(), eventId)); + } + // меняем состояние только у комментариев ожидающих модерацмм + if (comment.getState().equals(CommentState.PENDING)) { + comment.setState(commentState); + } + Comment savedComment = commentRepository.save(comment); + CommentDto commentDto = CommentMapper.toDto(savedComment); + if (commentDto.getState().equals(CommentState.APPROVED)) { + cgDto.getApprovedComments().add(commentDto); + } + if (commentDto.getState().equals(CommentState.REJECTED)) { + cgDto.getRejectedComments().add(commentDto); + } + } + return cgDto; + } + @Override public CommentDto getCommentById(Integer commentId) { Comment comment = commentRepository.findById(commentId) diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index d470393..0a597ba 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -74,8 +74,9 @@ CREATE TABLE IF NOT EXISTS comments author_id INTEGER NOT NULL, event_id INTEGER NOT NULL, text VARCHAR(2000) NOT NULL, + state VARCHAR(20) NOT NULL, created_on TIMESTAMP WITHOUT TIME ZONE, edited_on TIMESTAMP WITHOUT TIME ZONE, CONSTRAINT pk_comment PRIMARY KEY (id), CONSTRAINT fk_event_for_comment FOREIGN KEY (event_id) REFERENCES events (id) -);); \ No newline at end of file +); \ No newline at end of file From fcf26b95ba443f0da82bc44be4312098d8f0f492 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 20 Jul 2025 22:31:38 +0700 Subject: [PATCH 06/10] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BE=D0=BA=20CommentServiceImpl.java?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Postman/feature.json | 565 ++++++++++++++++++ .../controller/PublicController.java | 10 +- .../controller/UserCommentController.java | 4 +- .../practicum/evmsevice/dto/CommentDto.java | 2 - .../evmsevice/dto/CommentsGroupDto.java | 1 + .../ru/practicum/evmsevice/model/Comment.java | 1 - .../repository/CommentRepository.java | 5 +- .../repository/CommentSpecification.java | 41 ++ .../evmsevice/service/CommentService.java | 19 +- .../evmsevice/service/CommentServiceImpl.java | 106 +++- 10 files changed, 738 insertions(+), 16 deletions(-) create mode 100644 Postman/feature.json create mode 100644 ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentSpecification.java diff --git a/Postman/feature.json b/Postman/feature.json new file mode 100644 index 0000000..dfa571d --- /dev/null +++ b/Postman/feature.json @@ -0,0 +1,565 @@ +{ + "info": { + "_postman_id": "a7bb2177-ee2a-4f24-a50e-e58ca6eefa20", + "name": "Test Explore With Me - Feature comments", + "description": "# 🚀 Get started here\n\nThis template guides you through CRUD operations (GET, POST, PUT, DELETE), variables, and tests.\n\n## 🔖 **How to use this template**\n\n#### **Step 1: Send requests**\n\nRESTful APIs allow you to perform CRUD operations using the POST, GET, PUT, and DELETE HTTP methods.\n\nThis collection contains each of these [request](https://learning.postman.com/docs/sending-requests/requests/) types. Open each request and click \"Send\" to see what happens.\n\n#### **Step 2: View responses**\n\nObserve the response tab for status code (200 OK), response time, and size.\n\n#### **Step 3: Send new Body data**\n\nUpdate or add new data in \"Body\" in the POST request. Typically, Body data is also used in PUT request.\n\n```\n{\n \"name\": \"Add your name in the body\"\n}\n\n ```\n\n#### **Step 4: Update the variable**\n\nVariables enable you to store and reuse values in Postman. We have created a [variable](https://learning.postman.com/docs/sending-requests/variables/) called `base_url` with the sample request [https://postman-api-learner.glitch.me](https://postman-api-learner.glitch.me). Replace it with your API endpoint to customize this collection.\n\n#### **Step 5: Add tests in the \"Scripts\" tab**\n\nAdding tests to your requests can help you confirm that your API is working as expected. You can write test scripts in JavaScript and view the output in the \"Test Results\" tab.\n\n\"\"\n\n## 💪 Pro tips\n\n- Use folders to group related requests and organize the collection.\n \n- Add more [scripts](https://learning.postman.com/docs/writing-scripts/intro-to-scripts/) to verify if the API works as expected and execute workflows.\n \n\n## 💡Related templates\n\n[API testing basics](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9a37a28-055b-49cd-8c7e-97494a21eb54&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[API documentation](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9c28f47-1253-44af-a2f3-20dce4da1f18&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[Authorization methods](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=31a9a6ed-4cdf-4ced-984c-d12c9aec1c27&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719)", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "39468895" + }, + "item": [ + { + "name": "Добавление нового пользователя", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.be.success; // код ответа должен быть 201 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " let user;\r", + " try {\r", + " user = rnd.getUser();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(user),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{{request_body}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/admin/users", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление нового комментария к событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем в пути запроса идентификатор события\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // Генерируем случайный текст комментария\r", + " let comment;\r", + " try {\r", + " comment = rnd.getComment();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(comment),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 201 и данные в формате json\", function () {\r", + " pm.response.to.be.success; // код ответа должен быть 'успешно' \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Модерация комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор события, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем идентификаторы в пути запроса\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " // создаем новый коментарий\r", + " comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({commentIds: [comment.id],\r", + " state: \"APPROVED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "API = class {\r", + " constructor(postman, verbose = false, baseUrl = \"http://localhost:8080\") {\r", + " this.baseUrl = baseUrl;\r", + " this.pm = postman;\r", + " this._verbose = verbose;\r", + " }\r", + "\r", + " async addUser(user, verbose=null) {\r", + " return this.post(\"/admin/users\", user, \"Ошибка при добавлении нового пользователя: \", verbose);\r", + " }\r", + "\r", + " async addCategory(category, verbose=null) {\r", + " return this.post(\"/admin/categories\", category, \"Ошибка при добавлении новой категории: \", verbose);\r", + " }\r", + "\r", + " async addEvent(userId, event, verbose=null) {\r", + " return this.post(\"/users/\" + userId + \"/events\", event, \"Ошибка при добавлении нового события: \", verbose);\r", + " }\r", + "\r", + " async addComment(userId, eventId, comment, verbose=null) {\r", + " return this.post(\"/users/\" + userId + \"/events/\" + eventId + \"/comments\", comment, \"Ошибка при добавлении нового комментария: \", verbose);\r", + " }\r", + "\r", + " async addCompilation(compilation, verbose=null) {\r", + " return this.post(\"/admin/compilations\", compilation, \"Ошибка при добавлении новой подборки: \", verbose);\r", + " }\r", + "\r", + " async publishParticipationRequest(eventId, userId, verbose=null) {\r", + " return this.post('/users/' + userId + '/requests?eventId=' + eventId, null, \"Ошибка при добавлении нового запроса на участие в событии\", verbose);\r", + " }\r", + "\r", + " async publishEvent(eventId, verbose=null) {\r", + " return this.patch('/admin/events/' + eventId, {stateAction: \"PUBLISH_EVENT\"}, \"Ошибка при публикации события\", verbose);\r", + " }\r", + " \r", + " async rejectEvent(eventId, verbose=null) {\r", + " return this.patch('/admin/events/' + eventId, {stateAction: \"REJECT_EVENT\"}, \"Ошибка при отмене события\", verbose);\r", + " }\r", + "\r", + " async acceptParticipationRequest(eventId, userId, reqId, verbose=null) {\r", + " return this.patch('/users/' + userId + '/events/' + eventId + '/requests', {requestIds:[reqId], status: \"CONFIRMED\"}, \"Ошибка при принятии заявки на участие в событии\", verbose);\r", + " }\r", + "\r", + " async findCategory(catId, verbose=null) {\r", + " return this.get('/categories/' + catId, null, \"Ошибка при поиске категории по id\", verbose);\r", + " }\r", + "\r", + " async findCompilation(compId, verbose=null) {\r", + " return this.get('/compilations/' + compId, null, \"Ошибка при поиске подборки по id\", verbose);\r", + " }\r", + "\r", + " async findEvent(eventId, verbose=null) {\r", + " return this.get('/events/' + eventId, null, \"Ошибка при поиске события по id\", verbose);\r", + " }\r", + "\r", + " async findUser(userId, verbose=null) {\r", + " return this.get('/admin/users?ids=' + userId, null, \"Ошибка при поиске пользователя по id\", verbose);\r", + " }\r", + "\r", + " async post(path, body, errorText = \"Ошибка при выполнении post-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"POST\", path, body, errorText, verbose);\r", + " }\r", + "\r", + " async patch(path, body = null, errorText = \"Ошибка при выполнении patch-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"PATCH\", path, body, errorText, verbose);\r", + " }\r", + "\r", + " async get(path, body = null, errorText = \"Ошибка при выполнении get-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"GET\", path, body, errorText, verbose);\r", + " }\r", + " async sendRequest(method, path, body=null, errorText = \"Ошибка при выполнении запроса: \", verbose=null) {\r", + " return new Promise((resolve, reject) => {\r", + " verbose = verbose == null ? this._verbose : verbose;\r", + " const request = {\r", + " url: this.baseUrl + path,\r", + " method: method,\r", + " body: body == null ? \"\" : JSON.stringify(body),\r", + " header: { \"Content-Type\": \"application/json\" },\r", + " };\r", + " if(verbose) {\r", + " console.log(\"Отправляю запрос: \", request);\r", + " }\r", + "\r", + " try {\r", + " this.pm.sendRequest(request, (error, response) => {\r", + " if(error || (response.code >= 400 && response.code <= 599)) {\r", + " let err = error ? error : JSON.stringify(response.json());\r", + " console.error(\"При выполнении запроса к серверу возникла ошика.\\n\", err,\r", + " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + \r", + " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));\r", + "\r", + " reject(new Error(errorText + err));\r", + " }\r", + " if(verbose) {\r", + " console.log(\"Результат обработки запроса: код состояния - \", response.code, \", тело: \", response.json());\r", + " }\r", + " if (response.stream.length === 0){\r", + " reject(new Error('Отправлено пустое тело ответа'))\r", + " }else{\r", + " resolve(response.json());\r", + " }\r", + " });\r", + " \r", + " } catch(err) {\r", + " if(verbose) {\r", + " console.error(errorText, err);\r", + " }\r", + " return Promise.reject(err);\r", + " }\r", + " });\r", + " }\r", + "};\r", + "\r", + "RandomUtils = class {\r", + " constructor() {}\r", + "\r", + " getUser() {\r", + " return {\r", + " name: pm.variables.replaceIn('{{$randomFullName}}'),\r", + " email: pm.variables.replaceIn('{{$randomEmail}}')\r", + " };\r", + " }\r", + "\r", + " getCategory() {\r", + " return {\r", + " name: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 10000 * Math.random()).toString()\r", + " };\r", + " }\r", + "\r", + " getComment() {\r", + " return {\r", + " text: pm.variables.replaceIn('{{$randomWord}}') + Math.floor(Math.random() * 10000 * Math.random()).toString()\r", + " };\r", + " }\r", + "\r", + " getEvent(categoryId) {\r", + " return {\r", + " annotation: pm.variables.replaceIn('{{$randomLoremParagraph}}'),\r", + " category: categoryId,\r", + " description: pm.variables.replaceIn('{{$randomLoremParagraphs}}'),\r", + " eventDate: this.getFutureDateTime(),\r", + " location: {\r", + " lat: parseFloat(pm.variables.replaceIn('{{$randomLatitude}}')),\r", + " lon: parseFloat(pm.variables.replaceIn('{{$randomLongitude}}')),\r", + " },\r", + " paid: pm.variables.replaceIn('{{$randomBoolean}}'),\r", + " participantLimit: pm.variables.replaceIn('{{$randomInt}}'),\r", + " requestModeration: pm.variables.replaceIn('{{$randomBoolean}}'),\r", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}'),\r", + " }\r", + " }\r", + "\r", + " getCompilation(...eventIds) {\r", + " return {\r", + " title: pm.variables.replaceIn('{{$randomLoremSentence}}').slice(0, 50),\r", + " pinned: pm.variables.replaceIn('{{$randomBoolean}}'),\r", + " events: eventIds\r", + " };\r", + " }\r", + "\r", + "\r", + " getFutureDateTime(hourShift = 5, minuteShift=0, yearShift=0) {\r", + " let moment = require('moment');\r", + "\r", + " let m = moment();\r", + " m.add(hourShift, 'hour');\r", + " m.add(minuteShift, 'minute');\r", + " m.add(yearShift, 'year');\r", + "\r", + " return m.format('YYYY-MM-DD HH:mm:ss');\r", + " }\r", + "\r", + " getWord(length = 1) {\r", + " let result = '';\r", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\r", + " const charactersLength = characters.length;\r", + " let counter = 0;\r", + " while (counter < length) {\r", + " result += characters.charAt(Math.floor(Math.random() * charactersLength));\r", + " counter += 1;\r", + " }\r", + " return result;\r", + " }\r", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "id", + "value": "1" + }, + { + "key": "base_url", + "value": "https://postman-rest-api-learner.glitch.me/" + }, + { + "key": "baseUrl", + "value": "", + "type": "default" + }, + { + "key": "uid", + "value": "" + }, + { + "key": "eid", + "value": "" + } + ] +} \ No newline at end of file diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index 5fd4020..cab2ff4 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*; import ru.practicum.evmsevice.client.StatsClient; import ru.practicum.evmsevice.dto.*; +import ru.practicum.evmsevice.enums.CommentState; import ru.practicum.evmsevice.enums.EventState; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.mapper.CategoryMapper; @@ -111,10 +112,17 @@ public CategoryDto findCategoryById(@PathVariable("catId") int catId) { @GetMapping("/events/{eventId}/comments") @ResponseStatus(HttpStatus.OK) public List getCommentsByEventId(@PathVariable Integer eventId, + @RequestParam(name = "text", required = false) String text, + @RequestParam(name = "authorIds", required = false) List authorIds, + @RequestParam(name = "rangeStart", required = false) String rangeStart, + @RequestParam(name = "rangeEnd", required = false) String rangeEnd, + @RequestParam(name = "state", defaultValue = "APPROVED") String state, + @RequestParam(name = "sort", defaultValue = "new") String sort, @RequestParam(name = "from", defaultValue = "0") Integer from, @RequestParam(name = "size", defaultValue = "10") Integer size) { + CommentState commentState = CommentState.from(state).orElse(CommentState.APPROVED); log.info("Поиск всех коментариев к событию id={}.", eventId); - return commentService.getCommentsByEventId(eventId, from, size); + return commentService.getCommentsByEventId(eventId, text, authorIds, rangeStart, rangeEnd, commentState, sort, from, size); } @GetMapping("/comments/{commentId}") diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java index daa3638..2f384ce 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java @@ -41,7 +41,7 @@ public CommentDto updateComment(@PathVariable Integer userId, } @PatchMapping("/{userId}/events/{eventId}/comments") - @ResponseStatus(HttpStatus.CREATED) + @ResponseStatus(HttpStatus.OK) public CommentsGroupDto addNewComment(@PathVariable Integer userId, @PathVariable Integer eventId, @RequestBody CommentModerationDto cmModDto) { @@ -56,7 +56,7 @@ public CommentsGroupDto addNewComment(@PathVariable Integer userId, public List getCommentsByUserId(@PathVariable Integer userId, @RequestParam(name = "from", defaultValue = "0") Integer from, @RequestParam(name = "size", defaultValue = "10") Integer size) { - log.info("Поиск всех коментариев пользователя id={}.", userId); + log.info("Поиск всех комментариев пользователя id={}.", userId); return commentService.getCommentsByUserId(userId, from, size); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java index 8d95ae2..12d11f0 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentDto.java @@ -1,8 +1,6 @@ package ru.practicum.evmsevice.dto; import com.fasterxml.jackson.annotation.JsonFormat; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java index 1e237e2..0f76dfe 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/CommentsGroupDto.java @@ -1,4 +1,5 @@ package ru.practicum.evmsevice.dto; + import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java index 50982fd..1f5fed2 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/model/Comment.java @@ -5,7 +5,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; import ru.practicum.evmsevice.enums.CommentState; -import ru.practicum.evmsevice.enums.EventState; import java.time.LocalDateTime; diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java index 97def1b..1c95ece 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentRepository.java @@ -1,11 +1,14 @@ package ru.practicum.evmsevice.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import ru.practicum.evmsevice.model.Comment; import java.util.List; -public interface CommentRepository extends JpaRepository { +public interface CommentRepository extends JpaRepository, + JpaSpecificationExecutor { List findAllByEventId(Integer eventId); + List findAllByAuthor_Id(Integer authorId); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentSpecification.java b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentSpecification.java new file mode 100644 index 0000000..18e55ea --- /dev/null +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/repository/CommentSpecification.java @@ -0,0 +1,41 @@ +package ru.practicum.evmsevice.repository; + +import org.springframework.data.jpa.domain.Specification; +import ru.practicum.evmsevice.enums.CommentState; +import ru.practicum.evmsevice.model.Comment; + +import java.time.LocalDateTime; +import java.util.List; + +public class CommentSpecification { + public static Specification commentEventIdEqual(Integer eventId) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.in(root.get("eventId")).value(eventId)); + } + + public static Specification commentStateEqual(CommentState state) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.in(root.get("state")).value(state)); + } + + public static Specification commentContains(String text) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.like(root.get("text"), "%" + text + "%")); + } + + public static Specification commentAuthorIdIn(List userIds) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.in(root.join("author").get("id")).value(userIds)); + } + + public static Specification commentCreatedAfter(LocalDateTime startDateTime) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.greaterThanOrEqualTo(root.get("createdOn"), startDateTime)); + } + + public static Specification commentCreatedBefore(LocalDateTime endDateTime) { + return ((root, query, criteriaBuilder) -> + criteriaBuilder.lessThan(root.get("createdOn"), endDateTime)); + } + +} diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java index 0dacb1d..0a0028a 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentService.java @@ -4,15 +4,30 @@ import ru.practicum.evmsevice.dto.CommentModerationDto; import ru.practicum.evmsevice.dto.CommentsGroupDto; import ru.practicum.evmsevice.dto.NewCommentDto; +import ru.practicum.evmsevice.enums.CommentState; import java.util.List; public interface CommentService { CommentDto addComment(Integer userId, Integer eventId, NewCommentDto commentDto); + CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto commentDto); + CommentsGroupDto moderationComments(Integer userId, Integer eventId, CommentModerationDto commentModerationDto); + CommentDto getCommentById(Integer commentId); - List getCommentsByEventId(Integer eventId, Integer from, Integer ize); - List getCommentsByUserId(Integer userId, Integer from, Integer ize); + + List getCommentsByEventId(Integer eventId, + String text, + List authorIds, + String rangeStart, + String rangeEnd, + CommentState state, + String sort, + Integer from, + Integer size); + + List getCommentsByUserId(Integer userId, Integer from, Integer size); + void deleteComment(Integer userId, Integer commentId); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java index 09b0d04..1c893a8 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/service/CommentServiceImpl.java @@ -1,6 +1,8 @@ package ru.practicum.evmsevice.service; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.evmsevice.dto.CommentDto; @@ -8,6 +10,8 @@ import ru.practicum.evmsevice.dto.CommentsGroupDto; import ru.practicum.evmsevice.dto.NewCommentDto; import ru.practicum.evmsevice.enums.CommentState; +import ru.practicum.evmsevice.enums.EventState; +import ru.practicum.evmsevice.exception.BadRequestException; import ru.practicum.evmsevice.exception.DataConflictException; import ru.practicum.evmsevice.exception.NotFoundException; import ru.practicum.evmsevice.exception.ValidationException; @@ -16,25 +20,46 @@ import ru.practicum.evmsevice.model.Event; import ru.practicum.evmsevice.model.User; import ru.practicum.evmsevice.repository.CommentRepository; +import ru.practicum.evmsevice.repository.CommentSpecification; import ru.practicum.evmsevice.repository.EventRepository; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; import java.util.List; @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class CommentServiceImpl implements CommentService { + private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final Integer COMMENT_PERIOD_HOURS = 72; private final UserService userService; private final EventRepository eventRepository; private final CommentRepository commentRepository; + /** + * Создаем новый комментарий + * + * @param userId - идентификатор пользователя + * @param eventId - идентификатор события + * @param commentDto - входящий объект комметнария + * @return - сохраненный объект комментария + */ @Override @Transactional public CommentDto addComment(Integer userId, Integer eventId, NewCommentDto commentDto) { User user = userService.getUserById(userId); Event event = eventRepository.findById(eventId) .orElseThrow(() -> new NotFoundException("Не найдено событие id=" + eventId)); + if (!event.getState().equals(EventState.PUBLISHED)) { + throw new ValidationException("Комментировать возможно только опубликованные события."); + } + if (event.getEventDate().plusHours(COMMENT_PERIOD_HOURS).isBefore(LocalDateTime.now())) { + throw new ValidationException("Комментировать событие возможно только в течении " + + COMMENT_PERIOD_HOURS + " часов от начала события."); + } Comment comment = CommentMapper.getComment(commentDto); comment.setAuthor(user); comment.setEventId(eventId); @@ -61,8 +86,9 @@ public CommentDto updateComment(Integer userId, Integer commentId, NewCommentDto /** * Модерация комментариев к событию - * @param userId - идентификатор инициатора события - * @param eventId - идентификатор события + * + * @param userId - идентификатор инициатора события + * @param eventId - идентификатор события * @param commentModerationDto - объект идентификаторов коментариев для модерации * @return - список комментариев с измененным статусом */ @@ -109,20 +135,86 @@ public CommentDto getCommentById(Integer commentId) { } @Override - public List getCommentsByEventId(Integer eventId, Integer from, Integer ize) { - return commentRepository.findAllByEventId(eventId).stream() + public List getCommentsByEventId(Integer eventId, + String text, + List authorIds, + String rangeStart, + String rangeEnd, + CommentState state, + String sort, + Integer from, + Integer size) { + LocalDateTime startDateTime = null; + LocalDateTime endDateTime = null; + + if (rangeStart != null && !rangeStart.isEmpty()) { + try { + startDateTime = LocalDateTime.parse(rangeStart, DATA_TIME_FORMATTER); + } catch (DateTimeParseException e) { + throw new ValidationException("Некорректный формат времени. " + e.getMessage()); + } + } + if (rangeEnd != null && !rangeEnd.isEmpty()) { + try { + endDateTime = LocalDateTime.parse(rangeEnd, DATA_TIME_FORMATTER); + } catch (DateTimeParseException e) { + throw new ValidationException("Некорректный формат времени. " + e.getMessage()); + } + } + if (startDateTime != null && endDateTime != null) { + if (startDateTime.isAfter(endDateTime)) { + throw new BadRequestException( + "Parameter: rangeStart, rangeEnd. " + + "Error: Введен некорректный интервал времени." + + ". Value: " + startDateTime.format(DATA_TIME_FORMATTER) + + ", " + endDateTime.format(DATA_TIME_FORMATTER) + ); + } + } + + Specification spec = Specification.where(null); + // Задаем спецификации для поиска комментариев к событию + spec = spec.and(CommentSpecification.commentEventIdEqual(eventId)); + // ...поиск Комментариев по тексту + if (text != null) { + spec = spec.and(CommentSpecification.commentContains(text)); + } + // ... поиск по списку идентификаторов авторов комментариев + if (authorIds != null) { + spec = spec.and(CommentSpecification.commentAuthorIdIn(authorIds)); + } + // поиск по времени создания комментария + if (startDateTime != null) { + spec = spec.and(CommentSpecification.commentCreatedAfter(startDateTime)); + } + if (endDateTime != null) { + spec = spec.and(CommentSpecification.commentCreatedBefore(endDateTime)); + } + spec = spec.and(CommentSpecification.commentStateEqual(state)); + List comments = new ArrayList<>(); + if (sort != null) { + if (sort.equalsIgnoreCase("OLD")) { + comments = commentRepository.findAll(spec, Sort.by("createdOn")); + } else { + comments = commentRepository.findAll(spec, Sort.by("createdOn").descending()); + } + } else { + comments = commentRepository.findAll(spec, Sort.by("createdOn").descending()); + } + + return comments.stream() .map(CommentMapper::toDto) .skip(from) - .limit(ize) + .limit(size) .toList(); } @Override - public List getCommentsByUserId(Integer userId, Integer from, Integer ize) { + public List getCommentsByUserId(Integer userId, Integer from, Integer size) { return commentRepository.findAllByAuthor_Id(userId).stream() .map(CommentMapper::toDto) .skip(from) - .limit(ize) + .limit(size) .toList(); } From 2d52b3d2a1361a4f38fd618be6e9c11d4db856de Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Tue, 22 Jul 2025 23:04:48 +0700 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B=20Postman?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Postman/feature.json | 300 +++++++++++++----- .../controller/UserCommentController.java | 2 +- .../evmsevice/dto/NewCommentDto.java | 6 +- 3 files changed, 223 insertions(+), 85 deletions(-) diff --git a/Postman/feature.json b/Postman/feature.json index dfa571d..12b6954 100644 --- a/Postman/feature.json +++ b/Postman/feature.json @@ -8,23 +8,8 @@ }, "item": [ { - "name": "Добавление нового пользователя", + "name": "Добавление нового комментария к событию", "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", - " pm.response.to.be.success; // код ответа должен быть 201 \r", - " pm.response.to.be.withBody; // ответ должен содержать тело\r", - " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", - "});\r", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, { "listen": "prerequest", "script": { @@ -33,18 +18,38 @@ " const api = new API(pm);\r", " const rnd = new RandomUtils();\r", "\r", - " let user;\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем в пути запроса идентификатор события\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // Генерируем случайный текст комментария\r", + " let comment;\r", " try {\r", - " user = rnd.getUser();\r", + " comment = rnd.getComment();\r", " } catch(err) {\r", " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", " }\r", "\r", + " // формируем тело запроса\r", " pm.request.body.update({\r", " mode: 'raw',\r", - " raw: JSON.stringify(user),\r", + " raw: JSON.stringify(comment),\r", " options: { raw: { language: 'json' } }\r", " });\r", + "\r", "};\r", "\r", "const interval = setInterval(() => {}, 1000);\r", @@ -52,6 +57,7 @@ "setTimeout(async () => \r", " {\r", " try {\r", + " // выполняем наш скрипт\r", " await main();\r", " } catch (e) {\r", " console.error(e);\r", @@ -65,35 +71,69 @@ "type": "text/javascript", "packages": {} } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 201 и данные в формате json\", function () {\r", + " pm.response.to.be.success; // код ответа должен быть 'успешно' \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } } ], "request": { "method": "POST", - "header": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], "body": { "mode": "raw", - "raw": "{{request_body}}", - "options": { - "raw": { - "language": "json" - } - } + "raw": "{{request_body}}" }, "url": { - "raw": "{{baseUrl}}/admin/users", + "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", "host": [ "{{baseUrl}}" ], "path": [ - "admin", - "users" + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } ] } }, "response": [] }, { - "name": "Добавление нового комментария к событию", + "name": "Модерация комментария", "event": [ { "listen": "prerequest", @@ -103,7 +143,7 @@ " const api = new API(pm);\r", " const rnd = new RandomUtils();\r", "\r", - " // Подготовка данных: Событие, инициатор, автор комментария\r", + " // Подготовка данных: Событие, инициатор события, автор комментария\r", " try {\r", " const user = await api.addUser(rnd.getUser());\r", " const category = await api.addCategory(rnd.getCategory());\r", @@ -111,30 +151,23 @@ " eventBody['requestModeration'] = true\r", " let event = await api.addEvent(user.id, eventBody);\r", " event = await api.publishEvent(event.id);\r", - " // устанавливаем в пути запроса идентификатор события\r", + " // устанавливаем идентификаторы в пути запроса\r", + " pm.collectionVariables.set('uid', user.id);\r", " pm.collectionVariables.set(\"eid\", event.id)\r", " // создаем объект автора комментария\r", " const commentUser = await api.addUser(rnd.getUser());\r", - " pm.collectionVariables.set('uid', commentUser.id);\r", - " } catch(err) {\r", - " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", - " }\r", - "\r", - " // Генерируем случайный текст комментария\r", - " let comment;\r", - " try {\r", - " comment = rnd.getComment();\r", + " // создаем новый комментарий\r", + " comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({commentIds: [comment.id],\r", + " state: \"APPROVED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", " } catch(err) {\r", " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", " }\r", - "\r", - " // формируем тело запроса\r", - " pm.request.body.update({\r", - " mode: 'raw',\r", - " raw: JSON.stringify(comment),\r", - " options: { raw: { language: 'json' } }\r", - " });\r", - "\r", "};\r", "\r", "const interval = setInterval(() => {}, 1000);\r", @@ -161,8 +194,8 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Ответ должен содержать код 201 и данные в формате json\", function () {\r", - " pm.response.to.be.success; // код ответа должен быть 'успешно' \r", + "pm.test(\"Ответ должен содержать код 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", " pm.response.to.be.withBody; // ответ должен содержать тело\r", " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", "});\r", @@ -174,7 +207,7 @@ } ], "request": { - "method": "POST", + "method": "PATCH", "header": [ { "key": "Content-Type", @@ -218,7 +251,7 @@ "response": [] }, { - "name": "Модерация комментария", + "name": "Поиск комментариев пользователя", "event": [ { "listen": "prerequest", @@ -228,7 +261,7 @@ " const api = new API(pm);\r", " const rnd = new RandomUtils();\r", "\r", - " // Подготовка данных: Событие, инициатор события, автор комментария\r", + " // Подготовка данных: Событие, инициатор события, автор комментария, комментарии\r", " try {\r", " const user = await api.addUser(rnd.getUser());\r", " const category = await api.addCategory(rnd.getCategory());\r", @@ -236,20 +269,16 @@ " eventBody['requestModeration'] = true\r", " let event = await api.addEvent(user.id, eventBody);\r", " event = await api.publishEvent(event.id);\r", - " // устанавливаем идентификаторы в пути запроса\r", - " pm.collectionVariables.set('uid', user.id);\r", - " pm.collectionVariables.set(\"eid\", event.id)\r", " // создаем объект автора комментария\r", " const commentUser = await api.addUser(rnd.getUser());\r", - " // создаем новый коментарий\r", - " comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", - " // формируем тело запроса\r", - " pm.request.body.update({\r", - " mode: 'raw',\r", - " raw: JSON.stringify({commentIds: [comment.id],\r", - " state: \"APPROVED\"}),\r", - " options: { raw: { language: 'json' } }\r", - " });\r", + " // устанавливаем идентификатор автора в пути запроса\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " // создаем новые комментарии\r", + " comment1 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " comment2 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " // одобряем добавленные комментарии\r", + " modComment = await api.approvedComment(user.id, event.id, comment1.id);\r", + " modComment = await api.approvedComment(user.id, event.id, comment2.id); \r", " } catch(err) {\r", " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", " }\r", @@ -279,10 +308,13 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Ответ должен содержать код 200 и данные в формате json\", function () {\r", + "const body = pm.response.json();\r", + "pm.test(\"Тело ответа должно быть массивом и количество элементов больше 1\", function () {\r", " pm.response.to.be.ok; // код ответа должен быть 200 \r", " pm.response.to.be.withBody; // ответ должен содержать тело\r", " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + " pm.expect(body).is.an('array'); // проверяем, что тело ответа является массивом \r", + " pm.expect(body.length).to.be.gte(1); // длина массива должна быть больше 1\r", "});\r", "" ], @@ -291,8 +323,11 @@ } } ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Content-Type", @@ -305,18 +340,16 @@ ], "body": { "mode": "raw", - "raw": "{{request_body}}" + "raw": "" }, "url": { - "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "raw": "{{baseUrl}}/users/:userId/comments", "host": [ "{{baseUrl}}" ], "path": [ "users", ":userId", - "events", - ":eventId", "comments" ], "variable": [ @@ -324,11 +357,118 @@ "key": "userId", "value": "{{uid}}", "description": "(Required) id текущего пользователя" - }, + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск комментариев к заданному событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор события, автор комментария, комментарии\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем идентификатор события в пути запроса\r", + " pm.collectionVariables.set('eid', event.id);\r", + " comment1 = await api.addComment(user.id, event.id, rnd.getComment());\r", + " modComment = await api.approvedComment(user.id, event.id, comment1.id);\r", + " // создаем объект автора Дополнительного комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " // устанавливаем идентификатор автора в пути запроса\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " comment2 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " modComment = await api.approvedComment(user.id, event.id, comment2.id); \r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const body = pm.response.json();\r", + "pm.test(\"Тело ответа должно быть массивом и количество элементов больше 1\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + " pm.expect(body).is.an('array'); // проверяем, что тело ответа является массивом \r", + " pm.expect(body.length).to.be.gte(1); // длина массива должна быть больше 1\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/events/:eventId/comments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events", + ":eventId", + "comments" + ], + "variable": [ { "key": "eventId", - "value": "{{eid}}", - "description": "(Required) id события" + "value": "{{eid}}" } ] } @@ -376,6 +516,10 @@ " async publishEvent(eventId, verbose=null) {\r", " return this.patch('/admin/events/' + eventId, {stateAction: \"PUBLISH_EVENT\"}, \"Ошибка при публикации события\", verbose);\r", " }\r", + "\r", + " async approvedComment(userId, eventId, commentId, verbose=null) {\r", + " return this.patch(\"/users/\" + userId + \"/events/\" + eventId + \"/comments\", {commentIds: [commentId], state: \"APPROVED\"}, \"Ошибка при модерации комментария.\", verbose);\r", + " }\r", " \r", " async rejectEvent(eventId, verbose=null) {\r", " return this.patch('/admin/events/' + eventId, {stateAction: \"REJECT_EVENT\"}, \"Ошибка при отмене события\", verbose);\r", @@ -429,7 +573,7 @@ " this.pm.sendRequest(request, (error, response) => {\r", " if(error || (response.code >= 400 && response.code <= 599)) {\r", " let err = error ? error : JSON.stringify(response.json());\r", - " console.error(\"При выполнении запроса к серверу возникла ошика.\\n\", err,\r", + " console.error(\"При выполнении запроса к серверу возникла ошибка.\\n\", err,\r", " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + \r", " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));\r", "\r", @@ -544,10 +688,6 @@ "key": "id", "value": "1" }, - { - "key": "base_url", - "value": "https://postman-rest-api-learner.glitch.me/" - }, { "key": "baseUrl", "value": "", diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java index 2f384ce..dfbce9d 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java @@ -45,7 +45,7 @@ public CommentDto updateComment(@PathVariable Integer userId, public CommentsGroupDto addNewComment(@PathVariable Integer userId, @PathVariable Integer eventId, @RequestBody CommentModerationDto cmModDto) { - log.info("Пользователь id={}модерирует комментарии к событию id={}. {}", + log.info("Пользователь id={} модерирует комментарии к событию id={}. {}", userId, eventId, cmModDto.toString()); return commentService.moderationComments(userId, eventId, cmModDto); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java index 60414d1..5b99ef5 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/dto/NewCommentDto.java @@ -2,13 +2,11 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; @Setter @Getter +@ToString @NoArgsConstructor @AllArgsConstructor public class NewCommentDto { From 7cc375d3a20d38f1918a94b4080cf4c38899804f Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 23 Jul 2025 22:41:09 +0700 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20Postman?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Postman/feature.json | 910 +++++++++++++++++- ewm-service/schema.png | Bin 62186 -> 0 bytes .../controller/PublicController.java | 2 +- .../controller/UserCommentController.java | 2 +- ewm-service/src/main/resources/schema.sql | 12 +- 5 files changed, 872 insertions(+), 54 deletions(-) delete mode 100644 ewm-service/schema.png diff --git a/Postman/feature.json b/Postman/feature.json index 12b6954..cdeacff 100644 --- a/Postman/feature.json +++ b/Postman/feature.json @@ -2,7 +2,7 @@ "info": { "_postman_id": "a7bb2177-ee2a-4f24-a50e-e58ca6eefa20", "name": "Test Explore With Me - Feature comments", - "description": "# 🚀 Get started here\n\nThis template guides you through CRUD operations (GET, POST, PUT, DELETE), variables, and tests.\n\n## 🔖 **How to use this template**\n\n#### **Step 1: Send requests**\n\nRESTful APIs allow you to perform CRUD operations using the POST, GET, PUT, and DELETE HTTP methods.\n\nThis collection contains each of these [request](https://learning.postman.com/docs/sending-requests/requests/) types. Open each request and click \"Send\" to see what happens.\n\n#### **Step 2: View responses**\n\nObserve the response tab for status code (200 OK), response time, and size.\n\n#### **Step 3: Send new Body data**\n\nUpdate or add new data in \"Body\" in the POST request. Typically, Body data is also used in PUT request.\n\n```\n{\n \"name\": \"Add your name in the body\"\n}\n\n ```\n\n#### **Step 4: Update the variable**\n\nVariables enable you to store and reuse values in Postman. We have created a [variable](https://learning.postman.com/docs/sending-requests/variables/) called `base_url` with the sample request [https://postman-api-learner.glitch.me](https://postman-api-learner.glitch.me). Replace it with your API endpoint to customize this collection.\n\n#### **Step 5: Add tests in the \"Scripts\" tab**\n\nAdding tests to your requests can help you confirm that your API is working as expected. You can write test scripts in JavaScript and view the output in the \"Test Results\" tab.\n\n\"\"\n\n## 💪 Pro tips\n\n- Use folders to group related requests and organize the collection.\n \n- Add more [scripts](https://learning.postman.com/docs/writing-scripts/intro-to-scripts/) to verify if the API works as expected and execute workflows.\n \n\n## 💡Related templates\n\n[API testing basics](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9a37a28-055b-49cd-8c7e-97494a21eb54&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[API documentation](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e9c28f47-1253-44af-a2f3-20dce4da1f18&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719) \n[Authorization methods](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=31a9a6ed-4cdf-4ced-984c-d12c9aec1c27&sourceTemplateId=ddb19591-3097-41cf-82af-c84273e56719)", + "description": "Коллекция тестов API работы с комменариями событий.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "39468895" }, @@ -132,6 +132,485 @@ }, "response": [] }, + { + "name": "Добавление пустого комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем в пути запроса идентификатор события\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // Генерируем случайный текст комментария\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({text: \"\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 400 и данные в формате json\", function () {\r", + " pm.response.to.be.badRequest; // код ответа должен быть 400\r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление комментария к неопубликованному событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " // устанавливаем в пути запроса идентификатор события\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // Генерируем случайный текст комментария\r", + " let comment;\r", + " try {\r", + " comment = rnd.getComment();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(comment),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 403 и данные в формате json\", function () {\r", + " pm.response.to.be.forbidden; // код ответа должен быть 403' \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление комментария к несуществующему событию", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " // устанавливаем в пути запроса идентификатор события\r", + " pm.collectionVariables.set(\"eid\", 10000)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // Генерируем случайный текст комментария\r", + " let comment;\r", + " try {\r", + " comment = rnd.getComment();\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify(comment),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 404 и данные в формате json\", function () {\r", + " pm.response.to.be.notFound; // код ответа должен быть \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Исправление комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " // создаем новый коментарий\r", + " comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " pm.collectionVariables.set('cmid', comment.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({text: \"Исправленный текст комментария.\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + "\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 200 и данные в формате json\", function () {\r", + " pm.response.to.be.success; // код ответа должен быть 200\r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/comments/:commentId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "comments", + ":commentId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "commentId", + "value": "{{cmid}}" + } + ] + } + }, + "response": [] + }, { "name": "Модерация комментария", "event": [ @@ -143,7 +622,125 @@ " const api = new API(pm);\r", " const rnd = new RandomUtils();\r", "\r", - " // Подготовка данных: Событие, инициатор события, автор комментария\r", + " // Подготовка данных: Событие, инициатор события, автор комментария\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // устанавливаем идентификаторы в пути запроса\r", + " pm.collectionVariables.set('uid', user.id);\r", + " pm.collectionVariables.set(\"eid\", event.id)\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " // создаем новый коментарий\r", + " comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " // формируем тело запроса\r", + " pm.request.body.update({\r", + " mode: 'raw',\r", + " raw: JSON.stringify({commentIds: [comment.id],\r", + " state: \"APPROVED\"}),\r", + " options: { raw: { language: 'json' } }\r", + " });\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код 200 и данные в формате json\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{{request_body}}" + }, + "url": { + "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "events", + ":eventId", + "comments" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}", + "description": "(Required) id текущего пользователя" + }, + { + "key": "eventId", + "value": "{{eid}}", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск комментариев пользователя", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор события, автор комментария, комментарии\r", " try {\r", " const user = await api.addUser(rnd.getUser());\r", " const category = await api.addCategory(rnd.getCategory());\r", @@ -151,20 +748,16 @@ " eventBody['requestModeration'] = true\r", " let event = await api.addEvent(user.id, eventBody);\r", " event = await api.publishEvent(event.id);\r", - " // устанавливаем идентификаторы в пути запроса\r", - " pm.collectionVariables.set('uid', user.id);\r", - " pm.collectionVariables.set(\"eid\", event.id)\r", " // создаем объект автора комментария\r", " const commentUser = await api.addUser(rnd.getUser());\r", - " // создаем новый комментарий\r", - " comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", - " // формируем тело запроса\r", - " pm.request.body.update({\r", - " mode: 'raw',\r", - " raw: JSON.stringify({commentIds: [comment.id],\r", - " state: \"APPROVED\"}),\r", - " options: { raw: { language: 'json' } }\r", - " });\r", + " // устанавливаем идентификатор автора в пути запроса\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " // создаем новые коментарии\r", + " comment1 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " comment2 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " // одобряем добавленные комментарии\r", + " modComment = await api.approvedComment(user.id, event.id, comment1.id);\r", + " modComment = await api.approvedComment(user.id, event.id, comment2.id); \r", " } catch(err) {\r", " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", " }\r", @@ -194,10 +787,13 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Ответ должен содержать код 200 и данные в формате json\", function () {\r", + "const body = pm.response.json();\r", + "pm.test(\"Тело ответа должно быть массивом и колличество элементов больше 1\", function () {\r", " pm.response.to.be.ok; // код ответа должен быть 200 \r", " pm.response.to.be.withBody; // ответ должен содержать тело\r", " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + " pm.expect(body).is.an('array'); // проверяем, что тело ответа является массивом \r", + " pm.expect(body.length).to.be.gte(1); // длина массива должна быть больше 1\r", "});\r", "" ], @@ -206,8 +802,11 @@ } } ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { - "method": "PATCH", + "method": "GET", "header": [ { "key": "Content-Type", @@ -220,18 +819,16 @@ ], "body": { "mode": "raw", - "raw": "{{request_body}}" + "raw": "" }, "url": { - "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "raw": "{{baseUrl}}/users/:userId/comments", "host": [ "{{baseUrl}}" ], "path": [ "users", ":userId", - "events", - ":eventId", "comments" ], "variable": [ @@ -239,11 +836,6 @@ "key": "userId", "value": "{{uid}}", "description": "(Required) id текущего пользователя" - }, - { - "key": "eventId", - "value": "{{eid}}", - "description": "(Required) id события" } ] } @@ -251,7 +843,7 @@ "response": [] }, { - "name": "Поиск комментариев пользователя", + "name": "Поиск комментариев к заданному событию", "event": [ { "listen": "prerequest", @@ -269,15 +861,15 @@ " eventBody['requestModeration'] = true\r", " let event = await api.addEvent(user.id, eventBody);\r", " event = await api.publishEvent(event.id);\r", - " // создаем объект автора комментария\r", + " // устанавливаем идентификатор события в пути запроса\r", + " pm.collectionVariables.set('eid', event.id);\r", + " comment1 = await api.addComment(user.id, event.id, rnd.getComment());\r", + " modComment = await api.approvedComment(user.id, event.id, comment1.id);\r", + " // создаем объект автора Дополнительного комментария\r", " const commentUser = await api.addUser(rnd.getUser());\r", " // устанавливаем идентификатор автора в пути запроса\r", " pm.collectionVariables.set('uid', commentUser.id);\r", - " // создаем новые комментарии\r", - " comment1 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", " comment2 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", - " // одобряем добавленные комментарии\r", - " modComment = await api.approvedComment(user.id, event.id, comment1.id);\r", " modComment = await api.approvedComment(user.id, event.id, comment2.id); \r", " } catch(err) {\r", " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", @@ -309,7 +901,7 @@ "script": { "exec": [ "const body = pm.response.json();\r", - "pm.test(\"Тело ответа должно быть массивом и количество элементов больше 1\", function () {\r", + "pm.test(\"Тело ответа должно быть массивом и колличество элементов больше 1\", function () {\r", " pm.response.to.be.ok; // код ответа должен быть 200 \r", " pm.response.to.be.withBody; // ответ должен содержать тело\r", " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", @@ -343,20 +935,19 @@ "raw": "" }, "url": { - "raw": "{{baseUrl}}/users/:userId/comments", + "raw": "{{baseUrl}}/events/:eventId/comments", "host": [ "{{baseUrl}}" ], "path": [ - "users", - ":userId", + "events", + ":eventId", "comments" ], "variable": [ { - "key": "userId", - "value": "{{uid}}", - "description": "(Required) id текущего пользователя" + "key": "eventId", + "value": "{{eid}}" } ] } @@ -364,7 +955,124 @@ "response": [] }, { - "name": "Поиск комментариев к заданному событию", + "name": "Поиск комментария по идентификатору", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " // Подготовка данных: Событие, инициатор события, автор комментария, комментарии\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " comment = await api.addComment(user.id, event.id, rnd.getComment());\r", + " modComment = await api.approvedComment(user.id, event.id, comment.id);\r", + " // устанавливаем идентификатор события в пути запроса\r", + " pm.collectionVariables.set('cmid', comment.id);\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " // выполняем наш скрипт\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const body = pm.response.json();\r", + "pm.test(\"Тело ответа должно быть в формате json\", function () {\r", + " pm.response.to.be.ok; // код ответа должен быть 200 \r", + " pm.response.to.be.withBody; // ответ должен содержать тело\r", + " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", + "});\r", + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Комментарий должны содержать поля: id, author, eventId, text, state\", function () {\r", + " pm.expect(target).to.have.property('id');\r", + " pm.expect(target).to.have.property('author');\r", + " pm.expect(target).to.have.property('eventId');\r", + " pm.expect(target).to.have.property('text');\r", + " pm.expect(target).to.have.property('state');\r", + "});\r", + "\r", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {\r", + " pm.expect(target.id).equal(pm.collectionVariables.get(\"cmid\"));\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseUrl}}/comments/:commentId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "comments", + ":commentId" + ], + "variable": [ + { + "key": "commentId", + "value": "{{cmid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Поиск комментария к событию по фильтру", "event": [ { "listen": "prerequest", @@ -388,10 +1096,10 @@ " modComment = await api.approvedComment(user.id, event.id, comment1.id);\r", " // создаем объект автора Дополнительного комментария\r", " const commentUser = await api.addUser(rnd.getUser());\r", - " // устанавливаем идентификатор автора в пути запроса\r", - " pm.collectionVariables.set('uid', commentUser.id);\r", " comment2 = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", " modComment = await api.approvedComment(user.id, event.id, comment2.id); \r", + " pm.request.removeQueryParams(['text', 'authorIds', 'state']);\r", + " pm.request.addQueryParams([`text=` + comment1.text, 'authorIds=' + comment1.author.id]);\r", " } catch(err) {\r", " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", " }\r", @@ -422,14 +1130,25 @@ "script": { "exec": [ "const body = pm.response.json();\r", - "pm.test(\"Тело ответа должно быть массивом и количество элементов больше 1\", function () {\r", + "pm.test(\"Тело ответа должно быть в формате json\", function () {\r", " pm.response.to.be.ok; // код ответа должен быть 200 \r", " pm.response.to.be.withBody; // ответ должен содержать тело\r", " pm.response.to.be.json; // тело ответа должно быть в формате JSON\r", - " pm.expect(body).is.an('array'); // проверяем, что тело ответа является массивом \r", - " pm.expect(body.length).to.be.gte(1); // длина массива должна быть больше 1\r", "});\r", - "" + "\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Комментарий должны содержать поля: id, author, eventId, text, state\", function () {\r", + " pm.expect(target[0]).to.have.property('id');\r", + " pm.expect(target[0]).to.have.property('author');\r", + " pm.expect(target[0]).to.have.property('eventId');\r", + " pm.expect(target[0]).to.have.property('text');\r", + " pm.expect(target[0]).to.have.property('state');\r", + "});\r", + "\r", + "pm.test(\"Должен быть найден только один комментарий по заданному фильтру\", function () {\r", + " pm.expect(target.length).to.eql(1);\r", + "});" ], "type": "text/javascript", "packages": {} @@ -474,6 +1193,97 @@ } }, "response": [] + }, + { + "name": "Удаление комментария", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const main = async () => {\r", + " const api = new API(pm);\r", + " const rnd = new RandomUtils();\r", + "\r", + " try {\r", + " const user = await api.addUser(rnd.getUser());\r", + " const category = await api.addCategory(rnd.getCategory());\r", + " let eventBody = rnd.getEvent(category.id);\r", + " eventBody['requestModeration'] = true\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " // создаем объект автора комментария\r", + " const commentUser = await api.addUser(rnd.getUser());\r", + " pm.collectionVariables.set('uid', commentUser.id);\r", + " // создаем новый коментарий\r", + " const comment = await api.addComment(commentUser.id, event.id, rnd.getComment());\r", + " pm.collectionVariables.set(\"cmid\", comment.id)\r", + " } catch(err) {\r", + " console.error(\"Ошибка при подготовке тестовых данных.\", err);\r", + " }\r", + "};\r", + "\r", + "const interval = setInterval(() => {}, 1000);\r", + "\r", + "setTimeout(async () => \r", + " {\r", + " try {\r", + " await main();\r", + " } catch (e) {\r", + " console.error(e);\r", + " } finally {\r", + " clearInterval(interval);\r", + " }\r", + " }, \r", + " 100 \r", + ");" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/:userId/comments/:commentId", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "users", + ":userId", + "comments", + ":commentId" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}" + }, + { + "key": "commentId", + "value": "{{cmid}}" + } + ] + } + }, + "response": [] } ], "event": [ @@ -533,6 +1343,10 @@ " return this.get('/categories/' + catId, null, \"Ошибка при поиске категории по id\", verbose);\r", " }\r", "\r", + " async findComment(commentId, verbose=null) {\r", + " return this.get('/comments/' + commentId, null, \"Ошибка при поиске комментария по id\", verbose);\r", + " }\r", + "\r", " async findCompilation(compId, verbose=null) {\r", " return this.get('/compilations/' + compId, null, \"Ошибка при поиске подборки по id\", verbose);\r", " }\r", @@ -573,7 +1387,7 @@ " this.pm.sendRequest(request, (error, response) => {\r", " if(error || (response.code >= 400 && response.code <= 599)) {\r", " let err = error ? error : JSON.stringify(response.json());\r", - " console.error(\"При выполнении запроса к серверу возникла ошибка.\\n\", err,\r", + " console.error(\"При выполнении запроса к серверу возникла ошика.\\n\", err,\r", " \"\\nДля отладки проблемы повторите такой же запрос к вашей программе \" + \r", " \"на локальном компьютере. Данные запроса:\\n\", JSON.stringify(request));\r", "\r", @@ -700,6 +1514,10 @@ { "key": "eid", "value": "" + }, + { + "key": "cmid", + "value": "" } ] } \ No newline at end of file diff --git a/ewm-service/schema.png b/ewm-service/schema.png deleted file mode 100644 index fdde26e7f031028631c38bd4458030fb4591b56f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62186 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 diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java index cab2ff4..3b93dd4 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/PublicController.java @@ -121,7 +121,7 @@ public List getCommentsByEventId(@PathVariable Integer eventId, @RequestParam(name = "from", defaultValue = "0") Integer from, @RequestParam(name = "size", defaultValue = "10") Integer size) { CommentState commentState = CommentState.from(state).orElse(CommentState.APPROVED); - log.info("Поиск всех коментариев к событию id={}.", eventId); + log.info("Поиск коментариев к событию id={}.", eventId); return commentService.getCommentsByEventId(eventId, text, authorIds, rangeStart, rangeEnd, commentState, sort, from, size); } diff --git a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java index dfbce9d..3560b10 100644 --- a/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java +++ b/ewm-service/src/main/java/ru/practicum/evmsevice/controller/UserCommentController.java @@ -61,7 +61,7 @@ public List getCommentsByUserId(@PathVariable Integer userId, } - @DeleteMapping("/{userId}/comment/{commentId}") + @DeleteMapping("/{userId}/comments/{commentId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteComment(@PathVariable Integer commentId, @PathVariable Integer userId) { diff --git a/ewm-service/src/main/resources/schema.sql b/ewm-service/src/main/resources/schema.sql index 0a597ba..5d03831 100644 --- a/ewm-service/src/main/resources/schema.sql +++ b/ewm-service/src/main/resources/schema.sql @@ -70,13 +70,13 @@ CREATE TABLE IF NOT EXISTS eventlinks CREATE TABLE IF NOT EXISTS comments ( - id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, - author_id INTEGER NOT NULL, - event_id INTEGER NOT NULL, - text VARCHAR(2000) NOT NULL, - state VARCHAR(20) NOT NULL, + id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + author_id INTEGER NOT NULL, + event_id INTEGER NOT NULL, + text VARCHAR(2000) NOT NULL, + state VARCHAR(20) NOT NULL, created_on TIMESTAMP WITHOUT TIME ZONE, - edited_on TIMESTAMP WITHOUT TIME ZONE, + edited_on TIMESTAMP WITHOUT TIME ZONE, CONSTRAINT pk_comment PRIMARY KEY (id), CONSTRAINT fk_event_for_comment FOREIGN KEY (event_id) REFERENCES events (id) ); \ No newline at end of file From 778f6161ad44adb6cfcfe8b98331484a12b70a99 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 23 Jul 2025 22:54:47 +0700 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20P?= =?UTF-8?q?ostman?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- {Postman => postman}/ewm-main-service.json | 0 {Postman => postman}/ewm-stat-service.json | 0 {Postman => postman}/feature.json | 0 4 files changed, 4 insertions(+), 1 deletion(-) rename {Postman => postman}/ewm-main-service.json (100%) rename {Postman => postman}/ewm-stat-service.json (100%) rename {Postman => postman}/feature.json (100%) diff --git a/README.md b/README.md index a6ee1ef..745597d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # java-explore-with-me -Итоговый проект учебного курса. май 2025. +Итоговый проект учебного курса. июль 2025. + +Pull request: +https://github.com/andrej1307/java-explore-with-me/pull/3 diff --git a/Postman/ewm-main-service.json b/postman/ewm-main-service.json similarity index 100% rename from Postman/ewm-main-service.json rename to postman/ewm-main-service.json diff --git a/Postman/ewm-stat-service.json b/postman/ewm-stat-service.json similarity index 100% rename from Postman/ewm-stat-service.json rename to postman/ewm-stat-service.json diff --git a/Postman/feature.json b/postman/feature.json similarity index 100% rename from Postman/feature.json rename to postman/feature.json From e8f917d47b55e47093acefab1d3dbd2a27b9c57a Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 23 Jul 2025 23:05:17 +0700 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20P?= =?UTF-8?q?ostman,=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20baseurl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- postman/feature.json | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/postman/feature.json b/postman/feature.json index cdeacff..a1eca19 100644 --- a/postman/feature.json +++ b/postman/feature.json @@ -105,9 +105,9 @@ "raw": "{{request_body}}" }, "url": { - "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "users", @@ -224,9 +224,9 @@ "raw": "{{request_body}}" }, "url": { - "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "users", @@ -348,9 +348,9 @@ "raw": "{{request_body}}" }, "url": { - "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "users", @@ -468,9 +468,9 @@ "raw": "{{request_body}}" }, "url": { - "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "users", @@ -586,9 +586,9 @@ "raw": "{{request_body}}" }, "url": { - "raw": "{{baseUrl}}/users/:userId/comments/:commentId", + "raw": "http://localhost:8080/users/:userId/comments/:commentId", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "users", @@ -702,9 +702,9 @@ "raw": "{{request_body}}" }, "url": { - "raw": "{{baseUrl}}/users/:userId/events/:eventId/comments", + "raw": "http://localhost:8080/users/:userId/events/:eventId/comments", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "users", @@ -822,9 +822,9 @@ "raw": "" }, "url": { - "raw": "{{baseUrl}}/users/:userId/comments", + "raw": "http://localhost:8080/users/:userId/comments", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "users", @@ -935,9 +935,9 @@ "raw": "" }, "url": { - "raw": "{{baseUrl}}/events/:eventId/comments", + "raw": "http://localhost:8080/events/:eventId/comments", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "events", @@ -1053,9 +1053,9 @@ "raw": "" }, "url": { - "raw": "{{baseUrl}}/comments/:commentId", + "raw": "http://localhost:8080/comments/:commentId", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "comments", @@ -1175,9 +1175,9 @@ "raw": "" }, "url": { - "raw": "{{baseUrl}}/events/:eventId/comments", + "raw": "http://localhost:8080/events/:eventId/comments", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "events", @@ -1261,9 +1261,9 @@ "method": "DELETE", "header": [], "url": { - "raw": "{{baseUrl}}/users/:userId/comments/:commentId", + "raw": "http://localhost:8080/users/:userId/comments/:commentId", "host": [ - "{{baseUrl}}" + "http://localhost:8080" ], "path": [ "users",