diff --git a/README.md b/README.md index 18a246e..fc7ccf0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,34 @@ # java-explore-with-me Template repository for ExploreWithMe project. + +#Ссылка на pull-request +https://github.com/KonneyJ/java-explore-with-me/pull/3 + +# Реализация дополнительной функциональности: likes +1) POST /users/{userId}/events/{eventId}/likes +- добавление лайка событию +2) DELETE /users/{userId}/events/{eventId}/likes/{likeId} +- удаление лайка +3) PATCH /users/{userId}/events/{eventId}/likes/{likeId} +- редактирование лайка +4) GET /users/{userId}/events/{eventId}/likes/{likeId} +- получение лайка по идентификатору +5) GET /users/{userId}/likes +- получение всех лайков пользователя +6) GET /users/{userId}/events/{eventId}/likes +- получение всех лайков события + +## Интеграция + +- Private API + +## Логика + +- Нельзя ставить лайк своему событию +- Лайк можно поставить только посещенному мероприятию +- Лайк можно поставить только через 3 часа после начала мероприятия +- Лайк можно поставить только опубликованному событию +- Удалять/обновлять лайки могут только пользователи, которые их поставили +- Нет ограничений на время редактирования лайка +- Лайк может посмотреть либо человек, который его поставил, либо владелец события +- Посмотреть все лайки может только владелец события diff --git a/main-service/src/main/java/ru/practicum/ewm/compilation/dto/CompilationMapper.java b/main-service/src/main/java/ru/practicum/ewm/compilation/dto/CompilationMapper.java index 3c92629..b915997 100644 --- a/main-service/src/main/java/ru/practicum/ewm/compilation/dto/CompilationMapper.java +++ b/main-service/src/main/java/ru/practicum/ewm/compilation/dto/CompilationMapper.java @@ -2,7 +2,7 @@ import org.springframework.stereotype.Component; import ru.practicum.ewm.compilation.Compilation; -import ru.practicum.ewm.event.dto.EventMapper; +import ru.practicum.ewm.event.mapper.EventMapper; import java.util.stream.Collectors; diff --git a/main-service/src/main/java/ru/practicum/ewm/event/dto/NewEventDto.java b/main-service/src/main/java/ru/practicum/ewm/event/dto/NewEventDto.java index 94c50a8..65cb92c 100644 --- a/main-service/src/main/java/ru/practicum/ewm/event/dto/NewEventDto.java +++ b/main-service/src/main/java/ru/practicum/ewm/event/dto/NewEventDto.java @@ -24,7 +24,6 @@ public class NewEventDto { private String annotation; @NotNull(message = "Поле category не может быть null") - //@NotBlank(message = "Поле category не может быть пустым") private Integer category; @NotNull(message = "Поле description не может быть null") @@ -37,7 +36,6 @@ public class NewEventDto { private LocalDateTime eventDate; @NotNull(message = "Поле location не может быть null") - //@NotBlank(message = "Поле location не может быть пустым") private Location location; @Builder.Default diff --git a/main-service/src/main/java/ru/practicum/ewm/event/dto/EventMapper.java b/main-service/src/main/java/ru/practicum/ewm/event/mapper/EventMapper.java similarity index 97% rename from main-service/src/main/java/ru/practicum/ewm/event/dto/EventMapper.java rename to main-service/src/main/java/ru/practicum/ewm/event/mapper/EventMapper.java index 53f9c19..fe2c3a0 100644 --- a/main-service/src/main/java/ru/practicum/ewm/event/dto/EventMapper.java +++ b/main-service/src/main/java/ru/practicum/ewm/event/mapper/EventMapper.java @@ -1,7 +1,8 @@ -package ru.practicum.ewm.event.dto; +package ru.practicum.ewm.event.mapper; import org.springframework.stereotype.Component; import ru.practicum.ewm.category.dto.CategoryMapper; +import ru.practicum.ewm.event.dto.*; import ru.practicum.ewm.event.model.Event; import ru.practicum.ewm.user.dto.UserMapper; diff --git a/main-service/src/main/java/ru/practicum/ewm/event/service/EventServiceImpl.java b/main-service/src/main/java/ru/practicum/ewm/event/service/EventServiceImpl.java index 843ecd9..9eaeeb1 100644 --- a/main-service/src/main/java/ru/practicum/ewm/event/service/EventServiceImpl.java +++ b/main-service/src/main/java/ru/practicum/ewm/event/service/EventServiceImpl.java @@ -15,6 +15,7 @@ import ru.practicum.ewm.category.Category; import ru.practicum.ewm.category.CategoryRepository; import ru.practicum.ewm.event.dto.*; +import ru.practicum.ewm.event.mapper.EventMapper; import ru.practicum.ewm.event.model.Event; import ru.practicum.ewm.event.model.Location; import ru.practicum.ewm.event.model.QEvent; @@ -249,7 +250,6 @@ public EventFullDto createEvent(int userId, NewEventDto newEventDto) { Event savedEvent = eventRepository.save(event); log.info("Событие успешно сохранено с id {}", savedEvent.getId()); EventFullDto eventFullDto = eventMapper.toEventFullDto(savedEvent); - //eventFullDto.setViews(0); return eventFullDto; } diff --git a/main-service/src/main/java/ru/practicum/ewm/like/Like.java b/main-service/src/main/java/ru/practicum/ewm/like/Like.java new file mode 100644 index 0000000..644bf7c --- /dev/null +++ b/main-service/src/main/java/ru/practicum/ewm/like/Like.java @@ -0,0 +1,42 @@ +package ru.practicum.ewm.like; + +import jakarta.persistence.*; +import lombok.*; +import ru.practicum.ewm.event.model.Event; +import ru.practicum.ewm.request.model.Request; +import ru.practicum.ewm.user.User; + +import java.time.LocalDateTime; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "likes") +@Builder +@ToString +public class Like { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "like_id") + private Integer id; + + @Column(name = "liked", nullable = false) + private Boolean liked; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "event_id") + private Event event; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "request_id") + private Request request; + + @Column(name = "created_date") + private LocalDateTime createdOn; +} diff --git a/main-service/src/main/java/ru/practicum/ewm/like/LikePrivateController.java b/main-service/src/main/java/ru/practicum/ewm/like/LikePrivateController.java new file mode 100644 index 0000000..cac9a30 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/ewm/like/LikePrivateController.java @@ -0,0 +1,79 @@ +package ru.practicum.ewm.like; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.ewm.like.dto.LikeFullDto; +import ru.practicum.ewm.like.dto.LikeShortDto; +import ru.practicum.ewm.like.dto.NewLikeDto; +import ru.practicum.ewm.like.service.LikeService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequestMapping(path = "/users/{userId}") +@RequiredArgsConstructor +public class LikePrivateController { + private final LikeService likeService; + + @PostMapping("/events/{eventId}/likes") + @ResponseStatus(HttpStatus.CREATED) + public LikeFullDto createLike(@PathVariable("userId") int userId, + @PathVariable("eventId") int eventId, + @Valid @RequestBody NewLikeDto newLikeDto) { + log.info("PRIVATE POST /users/{userId}/events/{eventId}/likes запрос with userId {}, eventId {}, newLikeDto {}", + userId, eventId, newLikeDto); + return likeService.createLike(userId, eventId, newLikeDto); + } + + @DeleteMapping("/events/{eventId}/likes/{likeId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteLike(@PathVariable("userId") int userId, + @PathVariable("eventId") int eventId, + @PathVariable("likeId") int likeId) { + log.info("PRIVATE DELETE /users/{userId}/events/{eventId}/likes/{likeId} запрос with userId {}, eventId {}," + + " likeId {}", userId, eventId, likeId); + likeService.deleteLike(userId, eventId, likeId); + } + + @PatchMapping("/events/{eventId}/likes/{likeId}") + public LikeFullDto updateLike(@PathVariable("userId") int userId, + @PathVariable("eventId") int eventId, + @PathVariable("likeId") int likeId) { + log.info("PRIVATE PATCH /users/{userId}/events/{eventId}/likes/{likeId} запрос with userId {}, eventId {}," + + " likeId {}", userId, eventId, likeId); + return likeService.updateLike(userId, eventId, likeId); + } + + @GetMapping("/events/{eventId}/likes/{likeId}") + public LikeFullDto getLikeById(@PathVariable("userId") int userId, + @PathVariable("eventId") int eventId, + @PathVariable("likeId") int likeId) { + log.info("PRIVATE GET /users/{userId}/events/{eventId}/likes/{likeId} запрос with userId {}, eventId {}," + + " likeId {}", userId, eventId, likeId); + return likeService.getLikeById(userId, eventId, likeId); + } + + @GetMapping("/events/{eventId}/likes") + public Collection getAllLikesByEvent(@PathVariable("userId") int userId, + @PathVariable("eventId") int eventId, + @RequestParam(defaultValue = "0") @PositiveOrZero int from, + @RequestParam(defaultValue = "10") @Positive int size) { + log.info("PRIVATE GET /users/{userId}/events/{eventId}/likes запрос with userId {}, eventId {}, from {}, " + + "size {}", userId, eventId, from, size); + return likeService.getAllLikesByEvent(userId, eventId, from, size); + } + + @GetMapping("/likes") + public Collection getAllLikesByUser(@PathVariable("userId") int userId, + @RequestParam(defaultValue = "0") @PositiveOrZero int from, + @RequestParam(defaultValue = "10") @Positive int size) { + log.info("PRIVATE GET /users/{userId}/likes запрос with userId {}, from {}, size {}", userId, from, size); + return likeService.getAllLikesByUser(userId, from, size); + } +} diff --git a/main-service/src/main/java/ru/practicum/ewm/like/LikeRepository.java b/main-service/src/main/java/ru/practicum/ewm/like/LikeRepository.java new file mode 100644 index 0000000..428c837 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/ewm/like/LikeRepository.java @@ -0,0 +1,14 @@ +package ru.practicum.ewm.like; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface LikeRepository extends JpaRepository { + + List findAllByEventId(int eventId, PageRequest page); + + List findAllByUserId(int userId, PageRequest page); + +} diff --git a/main-service/src/main/java/ru/practicum/ewm/like/dto/LikeFullDto.java b/main-service/src/main/java/ru/practicum/ewm/like/dto/LikeFullDto.java new file mode 100644 index 0000000..3dcbb7a --- /dev/null +++ b/main-service/src/main/java/ru/practicum/ewm/like/dto/LikeFullDto.java @@ -0,0 +1,30 @@ +package ru.practicum.ewm.like.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class LikeFullDto { + private Integer id; + + @NotNull(message = "Поле liked не может быть null") + private Boolean liked; + + private Integer user; + + private Integer event; + + private Integer request; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime created; +} diff --git a/main-service/src/main/java/ru/practicum/ewm/like/dto/LikeMapper.java b/main-service/src/main/java/ru/practicum/ewm/like/dto/LikeMapper.java new file mode 100644 index 0000000..966042a --- /dev/null +++ b/main-service/src/main/java/ru/practicum/ewm/like/dto/LikeMapper.java @@ -0,0 +1,26 @@ +package ru.practicum.ewm.like.dto; + +import org.springframework.stereotype.Component; +import ru.practicum.ewm.like.Like; + +@Component +public class LikeMapper { + public static LikeFullDto toFullDto(Like like) { + return LikeFullDto.builder() + .id(like.getId()) + .liked(like.getLiked()) + .user(like.getUser().getId()) + .event(like.getEvent().getId()) + .request(like.getRequest().getId()) + .created(like.getCreatedOn()) + .build(); + } + + public static LikeShortDto toShortDto(Like like) { + return LikeShortDto.builder() + .id(like.getId()) + .liked(like.getLiked()) + .created(like.getCreatedOn()) + .build(); + } +} diff --git a/main-service/src/main/java/ru/practicum/ewm/like/dto/LikeShortDto.java b/main-service/src/main/java/ru/practicum/ewm/like/dto/LikeShortDto.java new file mode 100644 index 0000000..5fcb7d4 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/ewm/like/dto/LikeShortDto.java @@ -0,0 +1,24 @@ +package ru.practicum.ewm.like.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class LikeShortDto { + private Integer id; + + @NotNull(message = "Поле liked не может быть null") + private Boolean liked; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime created; +} diff --git a/main-service/src/main/java/ru/practicum/ewm/like/dto/NewLikeDto.java b/main-service/src/main/java/ru/practicum/ewm/like/dto/NewLikeDto.java new file mode 100644 index 0000000..409b521 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/ewm/like/dto/NewLikeDto.java @@ -0,0 +1,22 @@ +package ru.practicum.ewm.like.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class NewLikeDto { + @NotNull(message = "Поле liked не может быть null") + private Boolean liked; + + @NotNull(message = "Поле user не может быть null") + private Integer user; + + @NotNull(message = "Поле event не может быть null") + private Integer event; +} diff --git a/main-service/src/main/java/ru/practicum/ewm/like/service/LikeService.java b/main-service/src/main/java/ru/practicum/ewm/like/service/LikeService.java new file mode 100644 index 0000000..3e973ec --- /dev/null +++ b/main-service/src/main/java/ru/practicum/ewm/like/service/LikeService.java @@ -0,0 +1,22 @@ +package ru.practicum.ewm.like.service; + +import ru.practicum.ewm.like.dto.LikeFullDto; +import ru.practicum.ewm.like.dto.LikeShortDto; +import ru.practicum.ewm.like.dto.NewLikeDto; + +import java.util.Collection; + +public interface LikeService { + + LikeFullDto createLike(int userId, int eventId, NewLikeDto newLikeDto); + + void deleteLike(int userId, int eventId, int likeId); + + LikeFullDto updateLike(int userId, int eventId, int likeId); + + LikeFullDto getLikeById(int userId, int eventId, int likeId); + + Collection getAllLikesByEvent(int userId, int eventId, int from, int size); + + Collection getAllLikesByUser(int userId, int from, int size); +} diff --git a/main-service/src/main/java/ru/practicum/ewm/like/service/LikeServiceImpl.java b/main-service/src/main/java/ru/practicum/ewm/like/service/LikeServiceImpl.java new file mode 100644 index 0000000..646ba24 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/ewm/like/service/LikeServiceImpl.java @@ -0,0 +1,190 @@ +package ru.practicum.ewm.like.service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import ru.practicum.ewm.like.dto.LikeFullDto; +import ru.practicum.ewm.like.dto.LikeShortDto; +import ru.practicum.ewm.like.dto.NewLikeDto; +import ru.practicum.ewm.like.dto.LikeMapper; +import ru.practicum.ewm.event.model.Event; +import ru.practicum.ewm.like.Like; +import ru.practicum.ewm.event.model.enums.EventState; +import ru.practicum.ewm.event.repository.EventRepository; +import ru.practicum.ewm.like.LikeRepository; +import ru.practicum.ewm.exceptions.ConflictException; +import ru.practicum.ewm.exceptions.IncorrectParamException; +import ru.practicum.ewm.exceptions.NotFoundException; +import ru.practicum.ewm.request.RequestRepository; +import ru.practicum.ewm.request.model.Request; +import ru.practicum.ewm.request.model.RequestStatus; +import ru.practicum.ewm.user.User; +import ru.practicum.ewm.user.UserRepository; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@AllArgsConstructor +public class LikeServiceImpl implements LikeService { + private final UserRepository userRepository; + private final EventRepository eventRepository; + private final RequestRepository requestRepository; + private final LikeRepository likeRepository; + private final LikeMapper likeMapper; + + @Override + public LikeFullDto createLike(int userId, int eventId, NewLikeDto newLikeDto) { + log.info("Создание нового лайка {} пользователем с id {} на событие с id {}", newLikeDto, userId, eventId); + User user = checkUserExist(userId); + Event event = checkEventExist(eventId); + + if (event.getInitiator().getId() == userId) { + throw new ConflictException("Нельзя ставить лайк своему событию"); + } + + checkTime(event.getEventDate()); + + if (!event.getState().equals(EventState.PUBLISHED)) { + throw new ConflictException("Лайк можно поставить только опубликованному событию"); + } + + Request request = requestRepository.findByRequesterIdAndEventId(userId, eventId); + if (!request.getStatus().equals(RequestStatus.CONFIRMED)) { + throw new ConflictException("Лайк можно поставить только посещенному мероприятию"); + } + Like like = new Like(); + like.setLiked(newLikeDto.getLiked()); + like.setUser(user); + like.setEvent(event); + like.setRequest(request); + like.setCreatedOn(LocalDateTime.now()); + Like savedLike = likeRepository.save(like); + log.info("Лайк успешно сохранен с id {}", savedLike.getId()); + LikeFullDto likeFullDto = likeMapper.toFullDto(savedLike); + return likeFullDto; + } + + @Override + public void deleteLike(int userId, int eventId, int likeId) { + log.info("Удаление лайка с id {} пользователя с id {} на событие с id {}", likeId, userId, eventId); + User user = checkUserExist(userId); + Like like = checkLikeExist(likeId); + + if (like.getUser().getId() != userId) { + throw new ConflictException("Пользователь может удалять только свои лайки"); + } + likeRepository.deleteById(likeId); + log.info("Лайк успешно удален"); + } + + @Override + public LikeFullDto updateLike(int userId, int eventId, int likeId) { + log.info("Обновление лайка с id {} пользователем с id {} на событие с id {}", likeId, userId, eventId); + User user = checkUserExist(userId); + Event event = checkEventExist(eventId); + Like like = checkLikeExist(likeId); + + if (like.getUser().getId() != userId) { + throw new ConflictException("Пользователь может обновлять только свои лайки"); + } + checkTime(event.getEventDate()); + + if (like.getLiked().equals(true)) { + like.setLiked(false); + } else { + like.setLiked(true); + } + like.setCreatedOn(LocalDateTime.now()); + + Like updatedLiked = likeRepository.save(like); + LikeFullDto likeFullDto = likeMapper.toFullDto(updatedLiked); + log.info("Лайк успешно обновлен {}", likeFullDto); + return likeFullDto; + } + + @Override + public LikeFullDto getLikeById(int userId, int eventId, int likeId) { + log.info("Поиск лайка с id {} пользователем с id {} на событие с id {}", likeId, userId, eventId); + User user = checkUserExist(userId); + Event event = checkEventExist(eventId); + Like like = checkLikeExist(likeId); + + if ((like.getUser().getId() != userId) && (event.getInitiator().getId() != userId)) { + throw new ConflictException("Лайк может посмотреть либо владелец события, либо человек, который его поставил"); + } + LikeFullDto likeFullDto = likeMapper.toFullDto(like); + log.info("Лайк успешно найден {}", likeFullDto); + return likeFullDto; + } + + @Override + public Collection getAllLikesByEvent(int userId, int eventId, int from, int size) { + log.info("Поиск всех лайков события с id {} пользователем с id {} with from {] and size {}", + eventId, userId, from, size); + User user = checkUserExist(userId); + Event event = checkEventExist(eventId); + + if (event.getInitiator().getId() != userId) { + throw new ConflictException("Только владелец события может посмотреть все лайки"); + } + + PageRequest page = PageRequest.of(from, size, Sort.by("id").ascending()); + List likes = likeRepository.findAllByEventId(eventId, page); + List likesDto = likes.stream() + .map(LikeMapper::toShortDto) + .collect(Collectors.toList()); + log.info("Лайки события успешно найдены"); + return likesDto; + } + + @Override + public Collection getAllLikesByUser(int userId, int from, int size) { + log.info("Поиск лайков пользователя с id {} with from {} and size {}", userId, from, size); + User user = checkUserExist(userId); + PageRequest page = PageRequest.of(from, size, Sort.by("id").ascending()); + List likes = likeRepository.findAllByUserId(userId, page); + List likesDto = likes.stream() + .map(LikeMapper::toShortDto) + .collect(Collectors.toList()); + log.info("Лайки события успешно найдены"); + return likesDto; + } + + private User checkUserExist(int userId) { + log.info("Проверка пользователя с id {}", userId); + User user = userRepository.findById(userId).orElseThrow( + () -> new NotFoundException("Пользователь с id = " + userId + " не найден")); + log.info("Пользователь успешно найден"); + return user; + } + + private Event checkEventExist(int eventId) { + log.info("Проверка события с id {}", eventId); + Event event = eventRepository.findById(eventId).orElseThrow( + () -> new NotFoundException("Событие с id = " + eventId + " не найдено")); + log.info("Событие успешно найдено"); + return event; + } + + private void checkTime(LocalDateTime eventDate) { + log.info("Проверка времени лайка"); + if (eventDate.isBefore(LocalDateTime.now().minusHours(3))) { + throw new IncorrectParamException("Лайк можно поставить только через 3 часа после начала события"); + } + log.info("Дата лайка прошла модерацию"); + } + + private Like checkLikeExist(int likeId) { + log.info("Проверка лайка с id {}", likeId); + Like like = likeRepository.findById(likeId).orElseThrow( + () -> new NotFoundException("Лайк с id = " + likeId + " не найден")); + log.info("Лайк успешно найден"); + return like; + } +} diff --git a/main-service/src/main/resources/schema.sql b/main-service/src/main/resources/schema.sql index 8a9beb1..ae7e6e1 100644 --- a/main-service/src/main/resources/schema.sql +++ b/main-service/src/main/resources/schema.sql @@ -5,6 +5,7 @@ DROP TABLE IF EXISTS events CASCADE; DROP TABLE IF EXISTS requests CASCADE; DROP TABLE IF EXISTS compilations CASCADE; DROP TABLE IF EXISTS compilation_events CASCADE; +DROP TABLE IF EXISTS likes CASCADE; CREATE TABLE IF NOT EXISTS users ( user_id int generated by default as identity primary key, @@ -59,4 +60,13 @@ CREATE TABLE IF NOT EXISTS compilation_events( compilation_id int not null REFERENCES compilations (compilation_id) ON update CASCADE, event_id int not null REFERENCES events (event_id) ON update CASCADE, primary key(compilation_id, event_id) +); + +CREATE TABLE IF NOT EXISTS likes( + like_id int generated by default as identity primary key, + liked boolean not null, + user_id int not null REFERENCES users (user_id) on delete cascade, + event_id int not null REFERENCES events (event_id) on delete cascade, + request_id int not null REFERENCES requests (request_id) on delete cascade, + created_date timestamp without time zone ); \ No newline at end of file diff --git a/postman/feature.json b/postman/feature.json new file mode 100644 index 0000000..0126c2a --- /dev/null +++ b/postman/feature.json @@ -0,0 +1,681 @@ +{ + "info": { + "_postman_id": "85e2fe72-d6e0-42e3-bd60-10c3b7fdf15d", + "name": "feature-rating-events", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "37945376" + }, + "item": [ + { + "name": "Подготовительные тесты", + "item": [ + { + "name": "Добавление 1-го пользователя", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {", + " pm.response.to.have.status(201);", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "", + "const source = JSON.parse(pm.request.body.raw);", + "const target = pm.response.json();", + "", + "pm.test(\"Пользователь должен содержать поля: id, name, email\", function () {", + "pm.expect(target).to.have.property('id');", + "pm.expect(target).to.have.property('name');", + "pm.expect(target).to.have.property('email');", + "});", + "", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {", + " pm.expect(target.id).to.not.be.null;", + " pm.expect(source.name).equal(target.name, 'Имя пользователя должно соответствовать отправленному в запросе');", + " pm.expect(source.email).equal(target.email, 'Почта пользователя должна соответствовать отправленной в запросе');", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Julie\",\n \"email\": \"julie@yandex.ru\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8080/admin/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление 2-го пользователя", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {", + " pm.response.to.have.status(201);", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "", + "const source = JSON.parse(pm.request.body.raw);", + "const target = pm.response.json();", + "", + "pm.test(\"Пользователь должен содержать поля: id, name, email\", function () {", + "pm.expect(target).to.have.property('id');", + "pm.expect(target).to.have.property('name');", + "pm.expect(target).to.have.property('email');", + "});", + "", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {", + " pm.expect(target.id).to.not.be.null;", + " pm.expect(source.name).equal(target.name, 'Имя пользователя должно соответствовать отправленному в запросе');", + " pm.expect(source.email).equal(target.email, 'Почта пользователя должна соответствовать отправленной в запросе');", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Dima\",\n \"email\": \"dima@yandex.ru\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8080/admin/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление 3-го пользователя", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {", + " pm.response.to.have.status(201);", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "", + "const source = JSON.parse(pm.request.body.raw);", + "const target = pm.response.json();", + "", + "pm.test(\"Пользователь должен содержать поля: id, name, email\", function () {", + "pm.expect(target).to.have.property('id');", + "pm.expect(target).to.have.property('name');", + "pm.expect(target).to.have.property('email');", + "});", + "", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {", + " pm.expect(target.id).to.not.be.null;", + " pm.expect(source.name).equal(target.name, 'Имя пользователя должно соответствовать отправленному в запросе');", + " pm.expect(source.email).equal(target.email, 'Почта пользователя должна соответствовать отправленной в запросе');", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Andrei\",\n \"email\": \"andrei@yandex.ru\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8080/admin/users", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Добавление новой категории", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {", + " pm.response.to.have.status(201);", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "", + "const source = JSON.parse(pm.request.body.raw);", + "const target = pm.response.json();", + "", + "pm.test(\"Категория должна содержать поля: id, name\", function () {", + "pm.expect(target).to.have.property('id');", + "pm.expect(target).to.have.property('name');", + "});", + "", + "pm.test(\"Данные в ответе должны соответствовать данным в запросе\", function () {", + " pm.expect(target.id).to.not.be.null;", + " pm.expect(source.name).equal(target.name, 'Название категории должно совпадать с данными в запросе');", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Танцы\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8080/admin/categories", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "categories" + ] + } + }, + "response": [] + }, + { + "name": "Добавление нового события", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {", + " pm.response.to.have.status(201); ", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});", + "", + "const source = JSON.parse(pm.request.body.raw);", + "const target = pm.response.json();", + "", + "pm.test(\"Событие должно содержать поля: id, title, annotation, category, paid, eventDate, initiator, description, participantLimit, state, createdOn, location, requestModeration\", function () {", + "pm.expect(target).to.have.property('id');", + "pm.expect(target).to.have.property('title');", + "pm.expect(target).to.have.property('annotation');", + "pm.expect(target).to.have.property('category');", + "pm.expect(target).to.have.property('paid');", + "pm.expect(target).to.have.property('eventDate');", + "pm.expect(target).to.have.property('initiator');", + "pm.expect(target).to.have.property('description');", + "pm.expect(target).to.have.property('participantLimit');", + "pm.expect(target).to.have.property('state');", + "pm.expect(target).to.have.property('createdOn');", + "pm.expect(target).to.have.property('location');", + "pm.expect(target).to.have.property('requestModeration');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"annotation\": \"Бачата вечеринка El Grande клуб Jenavi\",\n \"category\": 122,\n \"description\": \"Бачата вечеринка El Grande пройдет 26 мая в клубе Jenavi в городе Санкт-Петербург. Возьмите с собой отличное настроение. Дресс-код - элегантный. \",\n \"eventDate\": \"2025-05-26 21:00:00\",\n \"location\": {\n \"lat\": 56.82,\n \"lon\": 23.54\n },\n \"requestModeration\": false,\n \"title\": \"Бачата вечеринка El Grande\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8080/users/137/events", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "137", + "events" + ] + } + }, + "response": [] + }, + { + "name": "Публикация события", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {", + " pm.response.to.be.ok; ", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"stateAction\": \"PUBLISH_EVENT\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/admin/events/:eventId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "events", + ":eventId" + ], + "variable": [ + { + "key": "eventId", + "value": "107" + } + ] + } + }, + "response": [] + }, + { + "name": "Добавление заявки на участие в событии", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "localhost:8080/users/138/requests?eventId=107", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "138", + "requests" + ], + "query": [ + { + "key": "eventId", + "value": "107", + "description": "(Required) id события" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Добавление нового лайка", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 201 и данные в формате json\", function () {\r", + " pm.response.to.have.status(201); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "\r", + "const source = JSON.parse(pm.request.body.raw);\r", + "const target = pm.response.json();\r", + "\r", + "pm.test(\"Лайк должен содержать поля: id, liked, user, event, request, created\", function () {\r", + "pm.expect(target).to.have.property('id');\r", + "pm.expect(target).to.have.property('liked');\r", + "pm.expect(target).to.have.property('user');\r", + "pm.expect(target).to.have.property('event');\r", + "pm.expect(target).to.have.property('request');\r", + "pm.expect(target).to.have.property('created');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"liked\": true,\r\n \"user\": 2,\r\n \"event\": 1\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8080/users/138/events/107/likes", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "138", + "events", + "107", + "likes" + ] + } + }, + "response": [] + }, + { + "name": "Обновление лайка", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [], + "url": { + "raw": "localhost:8080/users/138/events/107/likes/1", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "138", + "events", + "107", + "likes", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Поиск лайка по id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8080/users/138/events/107/likes/1", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "138", + "events", + "107", + "likes", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Поиск всех лайков события", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8080/users/137/events/107/likes", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "137", + "events", + "107", + "likes" + ] + } + }, + "response": [] + }, + { + "name": "Поиск всех лайков пользователя", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {\r", + " pm.response.to.have.status(200); \r", + " pm.response.to.be.withBody;\r", + " pm.response.to.be.json;\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8080/users/138/likes", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "138", + "likes" + ] + } + }, + "response": [] + }, + { + "name": "Удаление лайка", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {\r", + " pm.response.to.have.status(204);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "localhost:8080/users/138/events/107/likes/1", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "138", + "events", + "107", + "likes", + "1" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file