diff --git a/README.md b/README.md index 18a246e..2410627 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # java-explore-with-me -Template repository for ExploreWithMe project. +https://github.com/Siustoster/java-explore-with-me/pull/3 diff --git a/main-service/src/main/java/ru/practicum/mainservice/controller/AdminUserController.java b/main-service/src/main/java/ru/practicum/mainservice/controller/AdminUserController.java index 65113a8..1a30bc9 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/controller/AdminUserController.java +++ b/main-service/src/main/java/ru/practicum/mainservice/controller/AdminUserController.java @@ -8,6 +8,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.practicum.mainservice.model.user.dto.UserDto; +import ru.practicum.mainservice.model.user.dto.UserDtoWithRating; import ru.practicum.mainservice.service.UserService; import java.util.List; @@ -40,4 +41,12 @@ public void deleteUser(@PathVariable @Positive Integer userId) { log.info("Контроллер админа получил запрос на удаление пользователя с id = {}", userId); userService.deleteUser(userId); } + + @GetMapping("/users/rating") + public List getUsersWithRating(@RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("Контроллер админа получил запрос на получение списка пользователей с информацией " + + "об их рейтинге и сортировкой по возрастанию рейтинга"); + return userService.getUsersWithRating(from, size); + } } diff --git a/main-service/src/main/java/ru/practicum/mainservice/controller/PrivateEventController.java b/main-service/src/main/java/ru/practicum/mainservice/controller/PrivateEventController.java index adfc0cf..058c06a 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/controller/PrivateEventController.java +++ b/main-service/src/main/java/ru/practicum/mainservice/controller/PrivateEventController.java @@ -1,20 +1,19 @@ package ru.practicum.mainservice.controller; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; 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.mainservice.model.event.dto.EventFullDto; -import ru.practicum.mainservice.model.event.dto.EventShortDto; -import ru.practicum.mainservice.model.event.dto.NewEventDto; -import ru.practicum.mainservice.model.event.dto.UpdateEventRequest; +import ru.practicum.mainservice.model.event.dto.*; import ru.practicum.mainservice.model.request.dto.EventRequestStatusUpdateRequest; import ru.practicum.mainservice.model.request.dto.EventRequestStatusUpdateResult; import ru.practicum.mainservice.model.request.dto.ParticipationRequestDto; import ru.practicum.mainservice.service.EventService; +import ru.practicum.mainservice.service.RateService; import ru.practicum.mainservice.service.ParticipationRequestService; @@ -27,6 +26,7 @@ public class PrivateEventController { private final EventService eventService; private final ParticipationRequestService participationRequestService; + private final RateService rateService; @GetMapping("/events") public List getUserEvents(@PathVariable @Positive Integer userId, @@ -77,4 +77,38 @@ public EventRequestStatusUpdateResult updateRequestsStatus( "пользователя с id={}", eventId, userId); return participationRequestService.updateRequestsStatus(userId, eventId, eventRequestStatusUpdateRequest); } + + @PutMapping("/events/{eventId}/likes") + @ResponseStatus(HttpStatus.CREATED) + public void putMark(@PathVariable @Positive Integer userId, + @PathVariable @Positive Integer eventId, + @RequestParam @NotNull Boolean score) { + log.info("Запрос от private контроллера от пользователя с id={} на добавление реакции по событию с id={}", userId, eventId); + rateService.putMark(userId, eventId, score); + } + + @DeleteMapping("/events/{eventId}/likes") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteMark(@PathVariable @Positive Integer userId, + @PathVariable @Positive Integer eventId) { + log.info("Запрос от private контроллера от пользователя с id={} на удаление реакции по событию с id={}", userId, eventId); + rateService.deleteMark(userId, eventId); + } + + @GetMapping("/events/{eventId}/rating") + public EventFullDtoWithRating getUserEventWithRating(@PathVariable @Positive Integer userId, + @PathVariable @Positive Integer eventId) { + log.info("Запрос от private контроллера от пользователя с id={} на получение подробной информации о событии " + + "с указанием рейтинга события и его инициатора", userId); + return eventService.getEventOfUserWithRating(userId, eventId); + } + + @GetMapping("/events/rating") + public List getEventsWithRating(@PathVariable @Positive Integer userId, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("Запрос от private контроллера от пользователя с id={} на получение списка событий с наличием рейтинга " + + "с указанием рейтингов событий и их инициаторов с сортировкой по убыванию рейтинга событий", userId); + return eventService.getEventsWithRating(userId, from, size); + } } diff --git a/main-service/src/main/java/ru/practicum/mainservice/mappers/EventMapper.java b/main-service/src/main/java/ru/practicum/mainservice/mappers/EventMapper.java index 4580df0..fb424ed 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/mappers/EventMapper.java +++ b/main-service/src/main/java/ru/practicum/mainservice/mappers/EventMapper.java @@ -5,9 +5,7 @@ import ru.practicum.mainservice.model.enums.EventState; import ru.practicum.mainservice.model.event.Event; import ru.practicum.mainservice.model.event.Location; -import ru.practicum.mainservice.model.event.dto.EventFullDto; -import ru.practicum.mainservice.model.event.dto.EventShortDto; -import ru.practicum.mainservice.model.event.dto.NewEventDto; +import ru.practicum.mainservice.model.event.dto.*; import ru.practicum.mainservice.model.user.User; import java.time.LocalDateTime; @@ -29,7 +27,8 @@ public static Event toEvent(NewEventDto newEventDto, User initiator, Category ca LocalDateTime.now(), newEventDto.getRequestModeration() != null ? newEventDto.getRequestModeration() : true, EventState.PENDING, - newEventDto.getTitle() != null ? newEventDto.getTitle() : "" + newEventDto.getTitle() != null ? newEventDto.getTitle() : "", + 0 ); } @@ -81,4 +80,41 @@ public static EventShortDto toEventShortDto(Event event) { 0 ); } + + public static EventFullDtoWithRating toEventFullDtoWithRating(Event event) { + return new EventFullDtoWithRating( + event.getId(), + event.getAnnotation(), + CategoryMapper.toCategoryDto(event.getCategory()), + event.getConfirmedRequests(), + event.getCreatedOn().format(Constants.DATE_TIME_FORMAT), + event.getDescription(), + event.getEventDate().format(Constants.DATE_TIME_FORMAT), + UserMapper.toUserShortDtoWithRating(event.getInitiator()), + LocationMapper.toLocationDto(event.getLocation()), + event.getPaid(), + event.getParticipantLimit(), + event.getPublishedOn().format(Constants.DATE_TIME_FORMAT), + event.getRequestModeration(), + event.getState().toString(), + event.getTitle(), + 0, + event.getRating() + ); + } + + public static EventShortDtoWithRating toEventShortDtoWithRating(Event event) { + return new EventShortDtoWithRating( + event.getId(), + event.getAnnotation(), + CategoryMapper.toCategoryDto(event.getCategory()), + event.getConfirmedRequests(), + event.getEventDate().format(Constants.DATE_TIME_FORMAT), + UserMapper.toUserShortDtoWithRating(event.getInitiator()), + event.getPaid(), + event.getTitle(), + 0, + event.getRating() + ); + } } diff --git a/main-service/src/main/java/ru/practicum/mainservice/mappers/UserMapper.java b/main-service/src/main/java/ru/practicum/mainservice/mappers/UserMapper.java index 9f646cc..dca3789 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/mappers/UserMapper.java +++ b/main-service/src/main/java/ru/practicum/mainservice/mappers/UserMapper.java @@ -2,7 +2,9 @@ import ru.practicum.mainservice.model.user.User; import ru.practicum.mainservice.model.user.dto.UserDto; +import ru.practicum.mainservice.model.user.dto.UserDtoWithRating; import ru.practicum.mainservice.model.user.dto.UserShortDto; +import ru.practicum.mainservice.model.user.dto.UserShortDtoWithRating; public class UserMapper { public static UserDto toUserDto(User user) { @@ -26,7 +28,25 @@ public static User toUser(UserDto userDto) { return new User( userDto.getId() != null ? userDto.getId() : 0, userDto.getName() != null ? userDto.getName() : "", - userDto.getEmail() != null ? (userDto.getEmail()) : "" + userDto.getEmail() != null ? (userDto.getEmail()) : "", + 0 + ); + } + + public static UserShortDtoWithRating toUserShortDtoWithRating(User user) { + return new UserShortDtoWithRating( + user.getId(), + user.getName(), + user.getRating() + ); + } + + public static UserDtoWithRating toUserDtoWithRating(User user) { + return new UserDtoWithRating( + user.getId(), + user.getName(), + user.getName(), + user.getRating() ); } } diff --git a/main-service/src/main/java/ru/practicum/mainservice/model/Constants.java b/main-service/src/main/java/ru/practicum/mainservice/model/Constants.java index fefd1dd..1c04c6f 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/model/Constants.java +++ b/main-service/src/main/java/ru/practicum/mainservice/model/Constants.java @@ -7,4 +7,6 @@ public class Constants { public static final int FREE_TIME_INTERVAL = 100; public static final int UPDATE_TIME_LIMIT_ADMIN = 1; public static final int UPDATE_TIME_LIMIT_USER = 2; + public static final int CHANGING_RATING_WHEN_CHANGING_MARK = 2; + public static final int RATING_CHANGE_AT_NEW_MARK_OR_DELETE_MARK = 1; } diff --git a/main-service/src/main/java/ru/practicum/mainservice/model/event/Event.java b/main-service/src/main/java/ru/practicum/mainservice/model/event/Event.java index 3277690..e3a6942 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/model/event/Event.java +++ b/main-service/src/main/java/ru/practicum/mainservice/model/event/Event.java @@ -60,4 +60,6 @@ public class Event { private EventState state; @Column(nullable = false) private String title; + @Column(nullable = false) + private int rating; } diff --git a/main-service/src/main/java/ru/practicum/mainservice/model/event/dto/EventFullDtoWithRating.java b/main-service/src/main/java/ru/practicum/mainservice/model/event/dto/EventFullDtoWithRating.java new file mode 100644 index 0000000..eb1e7b6 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/mainservice/model/event/dto/EventFullDtoWithRating.java @@ -0,0 +1,32 @@ +package ru.practicum.mainservice.model.event.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.mainservice.model.category.dto.CategoryDto; +import ru.practicum.mainservice.model.user.dto.UserShortDtoWithRating; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class EventFullDtoWithRating { + private Integer id; + private String annotation; + private CategoryDto category; + private Integer confirmedRequests; + private String createdOn; + private String description; + private String eventDate; + private UserShortDtoWithRating initiator; + private LocationDto location; + private Boolean paid; + private int participantLimit; + private String publishedOn; + private Boolean requestModeration; + private String state; + private String title; + private int views; + private int rating; +} \ No newline at end of file diff --git a/main-service/src/main/java/ru/practicum/mainservice/model/event/dto/EventShortDtoWithRating.java b/main-service/src/main/java/ru/practicum/mainservice/model/event/dto/EventShortDtoWithRating.java new file mode 100644 index 0000000..4dae80d --- /dev/null +++ b/main-service/src/main/java/ru/practicum/mainservice/model/event/dto/EventShortDtoWithRating.java @@ -0,0 +1,25 @@ +package ru.practicum.mainservice.model.event.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.mainservice.model.category.dto.CategoryDto; +import ru.practicum.mainservice.model.user.dto.UserShortDtoWithRating; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class EventShortDtoWithRating { + protected Integer id; + protected String annotation; + protected CategoryDto category; + protected Integer confirmedRequests; + protected String eventDate; + protected UserShortDtoWithRating initiator; + protected Boolean paid; + protected String title; + protected int views; + protected int rating; +} diff --git a/main-service/src/main/java/ru/practicum/mainservice/model/rate/Rate.java b/main-service/src/main/java/ru/practicum/mainservice/model/rate/Rate.java new file mode 100644 index 0000000..1cdc996 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/mainservice/model/rate/Rate.java @@ -0,0 +1,35 @@ +package ru.practicum.mainservice.model.rate; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.type.NumericBooleanConverter; +import ru.practicum.mainservice.model.event.Event; +import ru.practicum.mainservice.model.user.User; + +@Entity +@Table(name = "MARKS", schema = "PUBLIC") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Rate { + @Id + @Column(name = "MARK_ID", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) + @JoinColumn(name = "EVALUATOR_ID") + private User evaluator; + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) + @JoinColumn(name = "EVENT_ID") + private Event event; + @Column(nullable = false) + @Convert(converter = NumericBooleanConverter.class) + private Boolean score; +} diff --git a/main-service/src/main/java/ru/practicum/mainservice/model/user/User.java b/main-service/src/main/java/ru/practicum/mainservice/model/user/User.java index b22e5c0..6db4710 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/model/user/User.java +++ b/main-service/src/main/java/ru/practicum/mainservice/model/user/User.java @@ -19,4 +19,6 @@ public class User { private String name; @Column(nullable = false) private String email; + @Column(nullable = false) + private int rating; } diff --git a/main-service/src/main/java/ru/practicum/mainservice/model/user/dto/UserDtoWithRating.java b/main-service/src/main/java/ru/practicum/mainservice/model/user/dto/UserDtoWithRating.java new file mode 100644 index 0000000..748422e --- /dev/null +++ b/main-service/src/main/java/ru/practicum/mainservice/model/user/dto/UserDtoWithRating.java @@ -0,0 +1,15 @@ +package ru.practicum.mainservice.model.user.dto; + +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class UserDtoWithRating { + private int id; + private String name; + private String email; + private int rating; +} \ No newline at end of file diff --git a/main-service/src/main/java/ru/practicum/mainservice/model/user/dto/UserShortDtoWithRating.java b/main-service/src/main/java/ru/practicum/mainservice/model/user/dto/UserShortDtoWithRating.java new file mode 100644 index 0000000..c429bfb --- /dev/null +++ b/main-service/src/main/java/ru/practicum/mainservice/model/user/dto/UserShortDtoWithRating.java @@ -0,0 +1,13 @@ +package ru.practicum.mainservice.model.user.dto; + +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class UserShortDtoWithRating { + private int id; + private String name; + private int rating; +} \ No newline at end of file diff --git a/main-service/src/main/java/ru/practicum/mainservice/repository/EventRepository.java b/main-service/src/main/java/ru/practicum/mainservice/repository/EventRepository.java index 2b1f9d2..a0862a3 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/repository/EventRepository.java +++ b/main-service/src/main/java/ru/practicum/mainservice/repository/EventRepository.java @@ -42,4 +42,9 @@ Page findByParametersForAdmin(@Param("users") List users, @Param("categories") List categories, @Param("start") LocalDateTime rangeStart, @Param("end") LocalDateTime rangeEnd, Pageable pageable); + + @Query("SELECT e FROM Event AS e " + + "WHERE e.rating <> 0 " + + "ORDER BY e.rating DESC") + Page findAllWhereRatingNotEqualToZeroSortByRatingDesc(Pageable pageable); } diff --git a/main-service/src/main/java/ru/practicum/mainservice/repository/RateRepository.java b/main-service/src/main/java/ru/practicum/mainservice/repository/RateRepository.java new file mode 100644 index 0000000..3629a46 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/mainservice/repository/RateRepository.java @@ -0,0 +1,9 @@ +package ru.practicum.mainservice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.mainservice.model.rate.Rate; + +public interface RateRepository extends JpaRepository { + + Rate findOneByEvaluator_IdAndEvent_Id(int evaluatorId, int eventId); +} diff --git a/main-service/src/main/java/ru/practicum/mainservice/repository/UserRepository.java b/main-service/src/main/java/ru/practicum/mainservice/repository/UserRepository.java index f2202fe..4c9c07d 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/repository/UserRepository.java +++ b/main-service/src/main/java/ru/practicum/mainservice/repository/UserRepository.java @@ -17,4 +17,9 @@ public interface UserRepository extends JpaRepository { Page findUsersForAdmin(@Param("ids") List ids, Pageable pageable); List findAllByEmail(String email); + + @Query("SELECT u FROM User AS u " + + "WHERE u.rating <> 0 " + + "ORDER BY u.rating") + Page findAllWhereRatingNotEqualToZeroSortByRating(Pageable pageable); } diff --git a/main-service/src/main/java/ru/practicum/mainservice/service/EventService.java b/main-service/src/main/java/ru/practicum/mainservice/service/EventService.java index a325f43..afd25b4 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/service/EventService.java +++ b/main-service/src/main/java/ru/practicum/mainservice/service/EventService.java @@ -26,10 +26,7 @@ import ru.practicum.mainservice.model.enums.UserStateAction; import ru.practicum.mainservice.model.event.Event; import ru.practicum.mainservice.model.event.Location; -import ru.practicum.mainservice.model.event.dto.EventFullDto; -import ru.practicum.mainservice.model.event.dto.EventShortDto; -import ru.practicum.mainservice.model.event.dto.NewEventDto; -import ru.practicum.mainservice.model.event.dto.UpdateEventRequest; +import ru.practicum.mainservice.model.event.dto.*; import ru.practicum.mainservice.model.user.User; import ru.practicum.mainservice.repository.EventRepository; import ru.practicum.mainservice.searchparams.PresentationParameters; @@ -372,4 +369,45 @@ public List getEventsWithFilteringForAdmin(SearchParametersAdmin s public Event getEvent(int eventId) { return eventRepository.getReferenceById(eventId); } + + @Transactional(readOnly = true) + public EventFullDtoWithRating getEventOfUserWithRating(int userId, int eventId) { + Event event = getEvent(eventId); + if (event.getInitiator().getId() != userId) { + throw new ConflictValidationException("Нельзя просматривать чужие события"); + } + List urisInList = new ArrayList<>(); + urisInList.add("/events/" + eventId); + String[] uris = urisInList.toArray(new String[0]); + Map statistic = getHitsStatistic(uris); + EventFullDtoWithRating eventFullDtoWithRating = EventMapper.toEventFullDtoWithRating(event); + if (statistic.get(eventFullDtoWithRating.getId()) != null) { + eventFullDtoWithRating.setViews(statistic.get(eventFullDtoWithRating.getId())); + } + return eventFullDtoWithRating; + } + + @Transactional(readOnly = true) + public List getEventsWithRating(int userId, int from, int size) { + User user = userService.getUser(userId); + Pageable pageable = PageRequest.of(from / size, size); + Page events; + events = eventRepository.findAllWhereRatingNotEqualToZeroSortByRatingDesc(pageable); + List shortEventsDtoWithRating = events + .stream() + .map(EventMapper::toEventShortDtoWithRating) + .collect(Collectors.toList()); + List urisInList = new ArrayList<>(); + for (EventShortDtoWithRating event : shortEventsDtoWithRating) { + urisInList.add("/events/" + event.getId()); + } + String[] uris = urisInList.toArray(new String[0]); + Map statistic = getHitsStatistic(uris); + for (EventShortDtoWithRating eventShotDtoWithRating : shortEventsDtoWithRating) { + if (statistic.get(eventShotDtoWithRating.getId()) != null) { + eventShotDtoWithRating.setViews(statistic.get(eventShotDtoWithRating.getId())); + } + } + return shortEventsDtoWithRating; + } } diff --git a/main-service/src/main/java/ru/practicum/mainservice/service/RateService.java b/main-service/src/main/java/ru/practicum/mainservice/service/RateService.java new file mode 100644 index 0000000..73212d8 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/mainservice/service/RateService.java @@ -0,0 +1,86 @@ +package ru.practicum.mainservice.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.mainservice.exception.BadRequestValidationException; +import ru.practicum.mainservice.model.Constants; +import ru.practicum.mainservice.model.event.Event; +import ru.practicum.mainservice.model.request.ParticipationRequest; +import ru.practicum.mainservice.model.user.User; +import ru.practicum.mainservice.repository.RateRepository; +import ru.practicum.mainservice.model.rate.Rate; +import ru.practicum.mainservice.model.enums.ParticipationRequestStatus; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RateService { + private final RateRepository rateRepository; + private final EventService eventService; + private final UserService userService; + private final ParticipationRequestService participationRequestService; + + @Transactional + public void putMark(int userId, int eventId, Boolean score) { + Rate mark = rateRepository.findOneByEvaluator_IdAndEvent_Id(userId, eventId); + Event event = eventService.getEvent(eventId); + User initiator = userService.getUser(event.getInitiator().getId()); + if (mark != null) { + if (!mark.getScore().toString().equals(score.toString())) { + if (mark.getScore()) { + event.setRating(event.getRating() - Constants.CHANGING_RATING_WHEN_CHANGING_MARK); + initiator.setRating(initiator.getRating() - Constants.CHANGING_RATING_WHEN_CHANGING_MARK); + } else { + event.setRating(event.getRating() + Constants.CHANGING_RATING_WHEN_CHANGING_MARK); + initiator.setRating(initiator.getRating() + Constants.CHANGING_RATING_WHEN_CHANGING_MARK); + } + mark.setScore(score); + } + } else { + ParticipationRequest request = participationRequestService.getRequestByEventAndRequester(eventId, userId); + if (request == null) { + throw new BadRequestValidationException("Оценивать событие могут только его участники"); + } + if (event.getParticipantLimit() == 0 || !event.getRequestModeration() || request.getStatus().equals(ParticipationRequestStatus.CONFIRMED)) { + User evaluator = userService.getUser(userId); + mark = new Rate(0, evaluator, event, score); + rateRepository.save(mark); + if (score) { + event.setRating(event.getRating() + Constants.RATING_CHANGE_AT_NEW_MARK_OR_DELETE_MARK); + initiator.setRating(initiator.getRating() + Constants.RATING_CHANGE_AT_NEW_MARK_OR_DELETE_MARK); + } else { + event.setRating(event.getRating() - Constants.RATING_CHANGE_AT_NEW_MARK_OR_DELETE_MARK); + initiator.setRating(initiator.getRating() - Constants.RATING_CHANGE_AT_NEW_MARK_OR_DELETE_MARK); + } + } else { + throw new BadRequestValidationException("Оценивать событие могут только его участники"); + } + } + eventService.save(event); + rateRepository.save(mark); + userService.saveUser(initiator); + } + + @Transactional + public void deleteMark(int userId, int eventId) { + Rate mark = rateRepository.findOneByEvaluator_IdAndEvent_Id(userId, eventId); + if (mark == null) { + return; + } + Event event = eventService.getEvent(eventId); + User initiator = userService.getUser(event.getInitiator().getId()); + if (mark.getScore()) { + event.setRating(event.getRating() - Constants.RATING_CHANGE_AT_NEW_MARK_OR_DELETE_MARK); + initiator.setRating(initiator.getRating() - Constants.RATING_CHANGE_AT_NEW_MARK_OR_DELETE_MARK); + } else { + event.setRating(event.getRating() + Constants.RATING_CHANGE_AT_NEW_MARK_OR_DELETE_MARK); + initiator.setRating(initiator.getRating() + Constants.RATING_CHANGE_AT_NEW_MARK_OR_DELETE_MARK); + } + eventService.save(event); + userService.saveUser(initiator); + rateRepository.delete(mark); + + } +} diff --git a/main-service/src/main/java/ru/practicum/mainservice/service/UserService.java b/main-service/src/main/java/ru/practicum/mainservice/service/UserService.java index 936a0be..52f4c3d 100644 --- a/main-service/src/main/java/ru/practicum/mainservice/service/UserService.java +++ b/main-service/src/main/java/ru/practicum/mainservice/service/UserService.java @@ -12,6 +12,7 @@ import ru.practicum.mainservice.mappers.UserMapper; import ru.practicum.mainservice.model.user.User; import ru.practicum.mainservice.model.user.dto.UserDto; +import ru.practicum.mainservice.model.user.dto.UserDtoWithRating; import ru.practicum.mainservice.repository.UserRepository; import java.util.List; @@ -60,4 +61,14 @@ public void deleteUser(int userId) { public User getUser(int userId) { return userRepository.getReferenceById(userId); } + + @Transactional(readOnly = true) + public List getUsersWithRating(int from, int size) { + Pageable pageable = PageRequest.of(from / size, size); + Page users; + users = userRepository.findAllWhereRatingNotEqualToZeroSortByRating(pageable); + return users.stream() + .map(UserMapper::toUserDtoWithRating) + .collect(Collectors.toList()); + } } diff --git a/main-service/src/main/resources/schema.sql b/main-service/src/main/resources/schema.sql index 9e0107b..f0a44d5 100644 --- a/main-service/src/main/resources/schema.sql +++ b/main-service/src/main/resources/schema.sql @@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS PUBLIC.USERS USER_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, NAME VARCHAR(250) NOT NULL, EMAIL VARCHAR(254) NOT NULL, + RATING INTEGER NOT NULL, CONSTRAINT UQ_USER_EMAIL UNIQUE (EMAIL) ); @@ -52,7 +53,8 @@ CREATE TABLE IF NOT EXISTS PUBLIC.EVENTS PUBLISHED_ON TIMESTAMP WITHOUT TIME ZONE, REQUEST_MODERATION INTEGER NOT NULL, STATE INTEGER NOT NULL, - TITLE VARCHAR(120) NOT NULL + TITLE VARCHAR(120) NOT NULL, + RATING INTEGER NOT NULL ); @@ -65,6 +67,15 @@ CREATE TABLE IF NOT EXISTS PUBLIC.PARTICIPATION_REQUESTS STATUS INTEGER NOT NULL ); +CREATE TABLE IF NOT EXISTS PUBLIC.MARKS +( + MARK_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + EVALUATOR_ID INTEGER REFERENCES PUBLIC.USERS (USER_ID) ON DELETE CASCADE, + EVENT_ID INTEGER REFERENCES PUBLIC.EVENTS (EVENT_ID) ON DELETE CASCADE, + SCORE INTEGER NOT NULL, + CONSTRAINT uq_evaluator_event UNIQUE (EVALUATOR_ID, EVENT_ID) +); + CREATE TABLE IF NOT EXISTS PUBLIC.EVENT_COMPILATIONS ( PAIR_ID INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, diff --git a/postman/feature.json b/postman/feature.json new file mode 100644 index 0000000..cc0cc42 --- /dev/null +++ b/postman/feature.json @@ -0,0 +1,624 @@ +{ + "info": { + "_postman_id": "1d725d38-0a3e-480d-98f8-39a6740f89c7", + "name": "feature", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "32844178" + }, + "item": [ + { + "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'] = false\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " await api.publishParticipationRequest(event.id,submittedUser.id);\r", + " pm.collectionVariables.set('uid',submittedUser.id);\r", + " pm.collectionVariables.set('eid',event.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": [ + "pm.test(\"Ответ должен содержать код статуса 201\", function () {", + " pm.response.to.have.status(201);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "http://localhost:8080/users/:userId/events/:eid/likes?score=true", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + ":userId", + "events", + ":eid", + "likes" + ], + "query": [ + { + "key": "score", + "value": "true" + } + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}" + }, + { + "key": "eid", + "value": "{{eid}}" + } + ] + } + }, + "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'] = false\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " await api.publishParticipationRequest(event.id,submittedUser.id);\r", + " pm.collectionVariables.set('uid',user.id);\r", + " pm.collectionVariables.set('eid',event.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": [ + "pm.test(\"Ответ должен содержать код статуса 200 и данные в формате json\", function () {", + " pm.response.to.have.status(200);", + " pm.response.to.be.withBody;", + " pm.response.to.be.json;", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/users/:uid/events/:eid/rating", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + ":uid", + "events", + ":eid", + "rating" + ], + "variable": [ + { + "key": "uid", + "value": "{{uid}}" + }, + { + "key": "eid", + "value": "{{eid}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение событий с рейтингом", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const body = pm.response.json();", + "", + "pm.test(\"Ответ должен содержать код статуса 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Ответ должен содержать список объектов\", function () {", + " pm.expect(body).is.an('array');", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/users/104/events/rating?from=0&size=20", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "104", + "events", + "rating" + ], + "query": [ + { + "key": "from", + "value": "0" + }, + { + "key": "size", + "value": "20" + } + ] + } + }, + "response": [] + }, + { + "name": "Получение пользователей с рейтингом", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const body = pm.response.json();", + "", + "pm.test(\"Ответ должен содержать код статуса 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Ответ должен содержать список объектов\", function () {", + " pm.expect(body).is.an('array');", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/admin/users/rating", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "admin", + "users", + "rating" + ] + } + }, + "response": [] + }, + { + "name": "Удаление лайка", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Ответ должен содержать код статуса 204\", function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "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'] = false\r", + " let event = await api.addEvent(user.id, eventBody);\r", + " event = await api.publishEvent(event.id);\r", + " const submittedUser = await api.addUser(rnd.getUser());\r", + " await api.publishParticipationRequest(event.id,submittedUser.id);\r", + " await api.addLike(event.id,submittedUser.id,\"true\");\r", + " pm.collectionVariables.set('uid',submittedUser.id);\r", + " pm.collectionVariables.set('eid', event.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": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/users/:userId/events/:eventId/likes", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + ":userId", + "events", + ":eventId", + "likes" + ], + "variable": [ + { + "key": "userId", + "value": "{{uid}}" + }, + { + "key": "eventId", + "value": "{{eid}}" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "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 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 addLike(eventId, userId, bLike, verbose=null) {\r", + " return this.put('/users/' + userId + '/events/' + eventId + '/likes?score=' + bLike, \"Ошибка при добавлении лайка\", verbose);\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 put(path, body, errorText = \"Ошибка при выполнении put-запроса: \", verbose=null) {\r", + " return this.sendRequest(\"PUT\", path, body, errorText, verbose);\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", + " 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", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "userId", + "value": "104" + }, + { + "key": "userId", + "value": "" + }, + { + "key": "uid", + "value": "" + }, + { + "key": "eid", + "value": "" + } + ] +} \ No newline at end of file