Skip to content

Commit

Permalink
add card update and action mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
alfabravo2013 committed Jan 21, 2024
1 parent 771c398 commit 39e901c
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import lombok.RequiredArgsConstructor;
import org.hyperskill.community.flashcards.card.mapper.CardMapper;
import org.hyperskill.community.flashcards.card.request.CardCreateRequest;
import org.hyperskill.community.flashcards.card.request.CardUpdateRequest;
import org.hyperskill.community.flashcards.card.response.CardResponse;
import org.hyperskill.community.flashcards.common.AuthenticationResolver;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand Down Expand Up @@ -39,7 +41,7 @@ public CardPageResponse getCards(
cardsPage.isLast(),
cardsPage.getNumber(),
cardsPage.getTotalPages(),
cardsPage.getContent().stream().map(mapper::map).toList()
cardsPage.getContent().stream().map(card -> mapper.map(card, categoryId)).toList()
);
}

Expand All @@ -53,7 +55,7 @@ public long getCardsCount(@RequestParam(name = "categoryId") String categoryId)
public CardResponse getCardById(@PathVariable String cardId, @RequestParam String categoryId) {
var username = authenticationResolver.resolveUsername();
var card = cardService.getCardById(username, cardId, categoryId);
return mapper.map(card);
return mapper.map(card, categoryId);
}

@DeleteMapping("/{cardId}")
Expand All @@ -72,4 +74,15 @@ public ResponseEntity<Void> createCard(
var uri = URI.create("/api/cards/" + id);
return ResponseEntity.created(uri).build();
}

@PutMapping("/{cardId}")
public CardResponse updateCardById(
@PathVariable String cardId,
@RequestParam String categoryId,
@Valid @RequestBody CardUpdateRequest request
) {
var username = authenticationResolver.resolveUsername();
var card = cardService.updateCardById(username, cardId, request, categoryId);
return mapper.map(card, categoryId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@
import org.hyperskill.community.flashcards.card.mapper.CardMapper;
import org.hyperskill.community.flashcards.card.mapper.CardReadConverter;
import org.hyperskill.community.flashcards.card.model.Card;
import org.hyperskill.community.flashcards.card.request.BaseCardUpdateRequest;
import org.hyperskill.community.flashcards.card.request.CardCreateRequest;
import org.hyperskill.community.flashcards.card.request.CardUpdateRequest;
import org.hyperskill.community.flashcards.card.request.MultipleChoiceQuizUpdateRequest;
import org.hyperskill.community.flashcards.card.request.QuestionAndAnswerUpdateRequest;
import org.hyperskill.community.flashcards.card.request.SingleChoiceQuizUpdateRequest;
import org.hyperskill.community.flashcards.category.CategoryService;
import org.hyperskill.community.flashcards.category.model.Category;
import org.hyperskill.community.flashcards.category.model.CategoryAccess;
import org.hyperskill.community.flashcards.common.exception.ResourceNotFoundException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
Expand All @@ -17,6 +24,7 @@
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;

import java.util.Objects;
Expand All @@ -33,19 +41,25 @@ public class CardService {
private final CardMapper mapper;

public Page<Card> getCardsByCategory(String username, String categoryId, int page) {
final var collection = getCollectionName(username, categoryId);
Objects.requireNonNull(categoryId, "Category ID cannot be null");

final var category = categoryService.findById(username, categoryId);

var aggregation = Aggregation.newAggregation(
Aggregation.sort(Sort.by("name")),
Aggregation.skip((long) page * PAGE_SIZE),
Aggregation.limit(PAGE_SIZE)
);

var count = mongoTemplate.count(new Query(), collection);
var result = mongoTemplate.aggregate(aggregation, collection, Document.class)
var count = mongoTemplate.count(new Query(), category.name());
var result = mongoTemplate.aggregate(aggregation, category.name(), Document.class)
.getRawResults()
.getList("results", Document.class);
var cards = result.stream().map(converter::convert).toList();

var permissions = getPermissions(username, category);
cards.forEach(card -> card.setPermissions(permissions));

return new PageImpl<>(cards, Pageable.ofSize(PAGE_SIZE), count);
}

Expand All @@ -62,11 +76,18 @@ public String createCard(String username, CardCreateRequest request, String cate

public Card getCardById(String username, String cardId, String categoryId) {
Objects.requireNonNull(cardId, "Card ID cannot be null");
Objects.requireNonNull(categoryId, "Category ID cannot be null");

var category = categoryService.findById(username, categoryId);

var collection = getCollectionName(username, categoryId);
return Optional.ofNullable(mongoTemplate.findById(cardId, Document.class, collection))
var card = Optional.ofNullable(mongoTemplate.findById(cardId, Document.class, category.name()))
.map(converter::convert)
.orElseThrow(ResourceNotFoundException::new);

var permissions = getPermissions(username, category);
card.setPermissions(permissions);

return card;
}

public void deleteCardById(String username, String cardId, String categoryId) {
Expand All @@ -77,14 +98,34 @@ public void deleteCardById(String username, String cardId, String categoryId) {
mongoTemplate.remove(query, collection);
}

public Card updateCardById(String username, String cardId, CardUpdateRequest request, String categoryId) {
Objects.requireNonNull(cardId, "Card ID cannot be null");
Objects.requireNonNull(categoryId, "Category ID cannot be null");

var category = categoryService.findById(username, categoryId, "w");

var query = Query.query(Criteria.where("_id").is(cardId));
mongoTemplate.updateFirst(query, updateFrom(request), category.name());

var updatedCard = Optional.ofNullable(mongoTemplate.findOne(query, Document.class, category.name()))
.map(converter::convert)
.orElseThrow(ResourceNotFoundException::new);

var permissions = getPermissions(username, category);
updatedCard.setPermissions(permissions);

return updatedCard;
}

private String getCollectionName(String username, String categoryId) {
return getCollectionName(username, categoryId, "r");
}

/**
* Gets a collection name if the provided user does have the specified permission. If the user
* doesn't have the permission, <code>AccessDeniedException</code> is thrown.
* @param username user's name
*
* @param username user's name
* @param categoryId ID of the requested category
* @param permission permission (r, w, or d),
* @return category name
Expand All @@ -94,4 +135,32 @@ private String getCollectionName(String username, String categoryId, String perm

return categoryService.findById(username, categoryId, permission).name();
}

private Update updateFrom(CardUpdateRequest request) {
Objects.requireNonNull(request, "Update request cannot be null");

var baseUpdateRequest = (BaseCardUpdateRequest) request;
var update = new Update()
.set("title", baseUpdateRequest.getTitle())
.set("question", baseUpdateRequest.getQuestion())
.set("tags", baseUpdateRequest.getTags());

return switch (request) {
case QuestionAndAnswerUpdateRequest qna -> update.set("answer", qna.getAnswer());
case SingleChoiceQuizUpdateRequest scq -> update.set("options", scq.getOptions())
.set("correctOption", scq.getCorrectOption());
case MultipleChoiceQuizUpdateRequest mcq -> update.set("options", mcq.getOptions())
.set("correctOptions", mcq.getCorrectOptions());
};
}

private String getPermissions(String username, Category category) {
return category.access().stream()
.filter(categoryAccess -> categoryAccess.username().equals(username))
.map(CategoryAccess::permission)
.findFirst().orElseThrow(() -> {
var message = "Category %s doesn't have access rules for user %s".formatted(category.id(), username);
return new IllegalStateException(message);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,23 @@
import org.hyperskill.community.flashcards.card.response.MultipleChoiceQuizDto;
import org.hyperskill.community.flashcards.card.response.QuestionAndAnswerDto;
import org.hyperskill.community.flashcards.card.response.SingleChoiceQuizDto;
import org.hyperskill.community.flashcards.common.ActionsParser;
import org.hyperskill.community.flashcards.common.response.PermittedAction;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
public class CardMapper {
public CardResponse map(Card card) {

public CardResponse map(Card card, String categoryId) {
var uri = "/api/cards/" + card.getId() + "?categoryId=" + categoryId;
var actions = ActionsParser.fromPermissions(card.getPermissions(), uri);

return switch (card) {
case QuestionAndAnswer qna -> toDto(qna);
case SingleChoiceQuiz scq -> toDto(scq);
case MultipleChoiceQuiz mcq -> toDto(mcq);
case QuestionAndAnswer qna -> toDto(qna, actions);
case SingleChoiceQuiz scq -> toDto(scq, actions);
case MultipleChoiceQuiz mcq -> toDto(mcq, actions);
};
}

Expand Down Expand Up @@ -63,40 +69,43 @@ private MultipleChoiceQuiz toDocument(MultipleChoiceQuizCreateRequest request) {
);
}

private QuestionAndAnswerDto toDto(QuestionAndAnswer card) {
private QuestionAndAnswerDto toDto(QuestionAndAnswer card, Set<PermittedAction> actions) {
return QuestionAndAnswerDto.builder()
.id(card.getId())
.title(card.getTitle())
.type("qna")
.question(card.getQuestion())
.answer(card.getAnswer())
.tags(card.getTags())
.actions(Set.of())
.actions(actions)
.createdAt(card.getCreatedAt())
.build();
}

private SingleChoiceQuizDto toDto(SingleChoiceQuiz card) {
private SingleChoiceQuizDto toDto(SingleChoiceQuiz card, Set<PermittedAction> actions) {
return SingleChoiceQuizDto.builder()
.id(card.getId())
.title(card.getTitle())
.type("scq")
.question(card.getQuestion())
.options(card.getOptions())
.correctOption(card.getCorrectOption())
.tags(card.getTags())
.actions(Set.of())
.actions(actions)
.createdAt(card.getCreatedAt())
.build();
}

private MultipleChoiceQuizDto toDto(MultipleChoiceQuiz card) {
private MultipleChoiceQuizDto toDto(MultipleChoiceQuiz card, Set<PermittedAction> actions) {
return MultipleChoiceQuizDto.builder()
.id(card.getId())
.title(card.getTitle())
.type("mcq")
.question(card.getQuestion())
.options(card.getOptions())
.correctOptions(card.getCorrectOptions())
.tags(card.getTags())
.actions(Set.of())
.actions(actions)
.createdAt(card.getCreatedAt())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.hyperskill.community.flashcards.card.mapper.MongoObjectIdConverter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;

import java.time.Instant;
Expand All @@ -33,6 +34,8 @@ public sealed abstract class Card permits QuestionAndAnswer, SingleChoiceQuiz, M
@CreatedDate
@JsonDeserialize(using = MongoDateConverter.class)
protected Instant createdAt;
@Transient
protected String permissions;

protected Card(String title, Set<String> tags, String question) {
this.title = title;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.hyperskill.community.flashcards.card.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

import java.util.Set;

@Getter
@Setter
public class BaseCardUpdateRequest {
@NotBlank
protected String title;
@NotNull
protected Set<@NotBlank String> tags;
@NotBlank
protected String question;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.hyperskill.community.flashcards.card.request;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type",
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = QuestionAndAnswerUpdateRequest.class, name = "qna"),
@JsonSubTypes.Type(value = SingleChoiceQuizUpdateRequest.class, name = "scq"),
@JsonSubTypes.Type(value = MultipleChoiceQuizUpdateRequest.class, name = "mcq"),
})
public sealed interface CardUpdateRequest permits
QuestionAndAnswerUpdateRequest,
SingleChoiceQuizUpdateRequest,
MultipleChoiceQuizUpdateRequest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.hyperskill.community.flashcards.card.request;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public final class MultipleChoiceQuizUpdateRequest extends BaseCardUpdateRequest implements CardUpdateRequest {
@NotNull
@NotEmpty
private List<@NotBlank String> options;
@NotNull
@NotEmpty
private List<@NotNull @Min(0) Integer> correctOptions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.hyperskill.community.flashcards.card.request;

import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public final class QuestionAndAnswerUpdateRequest extends BaseCardUpdateRequest implements CardUpdateRequest {
@NotBlank
private String answer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.hyperskill.community.flashcards.card.request;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public final class SingleChoiceQuizUpdateRequest extends BaseCardUpdateRequest implements CardUpdateRequest {
@NotNull
@NotEmpty
private List<@NotBlank String> options;
@NotNull
@Min(0)
private Integer correctOption;
}
Loading

0 comments on commit 39e901c

Please sign in to comment.