ImageView.viewers only non-empty when principal is owner. Added preparation, cook, and total time to Recipe.

This commit is contained in:
Jesse Brault 2024-08-12 20:05:24 -05:00
parent 22fac36e4b
commit ccae29b202
14 changed files with 310 additions and 82 deletions

View File

@ -46,6 +46,9 @@ public class RecipeControllerTests {
final RecipeCreateSpec spec = new RecipeCreateSpec();
spec.setSlug(slug);
spec.setTitle("Test Recipe");
spec.setPreparationTime(10);
spec.setCookingTime(20);
spec.setTotalTime(30);
spec.setRawText("# Hello, World!");
spec.setPublic(isPublic);
return this.recipeService.create(owner, spec);
@ -63,8 +66,13 @@ public class RecipeControllerTests {
this.mockMvc.perform(get("/recipes/{username}/{slug}", recipe.getOwner().getUsername(), recipe.getSlug()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.created").exists()) // TODO: better matching of exact LocalDateTime
.andExpect(jsonPath("$.modified").doesNotExist())
.andExpect(jsonPath("$.slug").value(recipe.getSlug()))
.andExpect(jsonPath("$.title").value("Test Recipe"))
.andExpect(jsonPath("$.preparationTime").value(recipe.getPreparationTime()))
.andExpect(jsonPath("$.cookingTime").value(recipe.getCookingTime()))
.andExpect(jsonPath("$.totalTime").value(recipe.getTotalTime()))
.andExpect(jsonPath("$.text").value("<h1>Hello, World!</h1>"))
.andExpect(jsonPath("$.owner.id").value(owner.getId()))
.andExpect(jsonPath("$.owner.username").value(owner.getUsername()))
@ -85,9 +93,13 @@ public class RecipeControllerTests {
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$.content[0].id").value(recipe.getId()))
.andExpect(jsonPath("$.content[0].updated").exists())
.andExpect(jsonPath("$.content[0].created").exists()) // TODO: better matching of exact LocalDateTime
.andExpect(jsonPath("$.content[0].modified").doesNotExist())
.andExpect(jsonPath("$.content[0].slug").value(recipe.getSlug()))
.andExpect(jsonPath("$.content[0].title").value(recipe.getTitle()))
.andExpect(jsonPath("$.content[0].preparationTime").value(recipe.getPreparationTime()))
.andExpect(jsonPath("$.content[0].cookingTime").value(recipe.getCookingTime()))
.andExpect(jsonPath("$.content[0].totalTime").value(recipe.getTotalTime()))
.andExpect(jsonPath("$.content[0].owner.id").value(owner.getId()))
.andExpect(jsonPath("$.content[0].owner.username").value(owner.getUsername()))
.andExpect(jsonPath("$.content[0].isPublic").value(true))

View File

@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -42,6 +43,7 @@ public class RecipeRepositoryTests {
@DirtiesContext
public void findsAllPublicRecipes() {
final RecipeEntity publicRecipe = new RecipeEntity();
publicRecipe.setCreated(LocalDateTime.now());
publicRecipe.setSlug("public-recipe");
publicRecipe.setPublic(true);
publicRecipe.setOwner(this.getOwnerUser());
@ -57,6 +59,7 @@ public class RecipeRepositoryTests {
@DirtiesContext
public void doesNotFindNonPublicRecipe() {
final RecipeEntity nonPublicRecipe = new RecipeEntity();
nonPublicRecipe.setCreated(LocalDateTime.now());
nonPublicRecipe.setSlug("non-public-recipe");
nonPublicRecipe.setOwner(this.getOwnerUser());
nonPublicRecipe.setTitle("Non-Public Recipe");
@ -71,6 +74,7 @@ public class RecipeRepositoryTests {
@DirtiesContext
public void findsAllForViewer() {
final RecipeEntity recipe = new RecipeEntity();
recipe.setCreated(LocalDateTime.now());
recipe.setSlug("test-recipe");
recipe.setOwner(this.getOwnerUser());
recipe.setTitle("Test Recipe");
@ -92,6 +96,7 @@ public class RecipeRepositoryTests {
@DirtiesContext
public void doesNotIncludeNonViewable() {
final RecipeEntity recipe = new RecipeEntity();
recipe.setCreated(LocalDateTime.now());
recipe.setSlug("test-recipe");
recipe.setOwner(this.getOwnerUser());
recipe.setTitle("Test Recipe");

View File

@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
// TODO: test mainImage included
// TODO: test prep/cooking/total times included
@SpringBootTest
public class RecipeServiceTests {

View File

@ -111,7 +111,7 @@ public class ImageController {
image.getSize(),
createSpec
);
return ResponseEntity.status(201).body(this.imageService.toImageView(saved));
return ResponseEntity.status(201).body(this.imageService.toImageView(saved, principal));
}
@PostMapping("/{username}/{filename}")
@ -127,7 +127,7 @@ public class ImageController {
final User owner = this.userService.getUser(username);
final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal);
final Image updated = this.imageService.update(image, principal, this.getImageUpdateSpec(body));
return ResponseEntity.ok(this.imageService.toImageView(updated));
return ResponseEntity.ok(this.imageService.toImageView(updated, principal));
}
@DeleteMapping("/{username}/{filename}")

View File

@ -25,6 +25,6 @@ public interface ImageService {
void deleteImage(Image image, User modifier) throws IOException;
ImageView toImageView(Image image);
ImageView toImageView(Image image, @Nullable User viewer);
}

View File

@ -6,7 +6,6 @@ import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.s3.S3Manager;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PostAuthorize;
@ -21,7 +20,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
/* TODO: update modified LocalDateTime when updating */
public class S3ImageService implements ImageService {
private static final Pattern extensionPattern = Pattern.compile(".+\\.(.+)$");
@ -186,34 +184,17 @@ public class S3ImageService implements ImageService {
this.s3Manager.delete("images", imageEntity.getObjectName());
}
private String getImageUrl(Image image) {
return this.baseUrl + "/images/" + image.getOwner().getUsername() + "/" + image.getUserFilename();
}
@Override
public ImageView toImageView(Image image) {
final ImageView imageView = new ImageView();
imageView.setUrl(this.baseUrl + "/images/" + image.getOwner().getUsername() + "/" + image.getUserFilename());
imageView.setCreated(image.getCreated());
imageView.setModified(image.getModified());
imageView.setFilename(image.getUserFilename());
imageView.setMimeType(image.getMimeType());
imageView.setAlt(image.getAlt());
imageView.setCaption(image.getCaption());
imageView.setIsPublic(image.isPublic());
final User owner = image.getOwner();
final UserInfoView userInfoView = new UserInfoView();
userInfoView.setId(owner.getId());
userInfoView.setUsername(owner.getUsername());
imageView.setOwner(userInfoView);
final Set<UserInfoView> viewers = new HashSet<>();
for (final User viewer : image.getViewers()) {
final UserInfoView viewerView = new UserInfoView();
viewerView.setId(viewer.getId());
viewerView.setUsername(viewer.getUsername());
viewers.add(viewerView);
public ImageView toImageView(Image image, @Nullable User viewer) {
if (viewer != null && image.getOwner().getUsername().equals(viewer.getUsername())) {
return ImageView.from(image, this.getImageUrl(image), true);
} else {
return ImageView.from(image, this.getImageUrl(image), false);
}
imageView.setViewers(viewers);
return imageView;
}
}

View File

@ -1,14 +1,35 @@
package app.mealsmadeeasy.api.image.view;
import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.stream.Collectors;
// TODO: get rid of viewers, keep it only for owner view!
public class ImageView {
public static ImageView from(Image image, String url, boolean includeViewers) {
final ImageView view = new ImageView();
view.setUrl(url);
view.setCreated(image.getCreated());
view.setModified(image.getModified());
view.setFilename(image.getUserFilename());
view.setMimeType(image.getMimeType());
view.setAlt(image.getAlt());
view.setCaption(image.getCaption());
view.setOwner(UserInfoView.from(image.getOwner()));
view.setIsPublic(image.isPublic());
if (includeViewers) {
view.setViewers(image.getViewers().stream()
.map(UserInfoView::from)
.collect(Collectors.toSet())
);
}
return view;
}
private String url;
private LocalDateTime created;
private @Nullable LocalDateTime modified;
@ -18,7 +39,7 @@ public class ImageView {
private @Nullable String caption;
private UserInfoView owner;
private boolean isPublic;
private Set<UserInfoView> viewers;
private @Nullable Set<UserInfoView> viewers;
public String getUrl() {
return this.url;
@ -60,19 +81,19 @@ public class ImageView {
this.mimeType = mimeType;
}
public String getAlt() {
public @Nullable String getAlt() {
return this.alt;
}
public void setAlt(String alt) {
public void setAlt(@Nullable String alt) {
this.alt = alt;
}
public String getCaption() {
public @Nullable String getCaption() {
return this.caption;
}
public void setCaption(String caption) {
public void setCaption(@Nullable String caption) {
this.caption = caption;
}
@ -92,11 +113,11 @@ public class ImageView {
this.isPublic = isPublic;
}
public Set<UserInfoView> getViewers() {
public @Nullable Set<UserInfoView> getViewers() {
return this.viewers;
}
public void setViewers(Set<UserInfoView> viewers) {
public void setViewers(@Nullable Set<UserInfoView> viewers) {
this.viewers = viewers;
}

View File

@ -15,6 +15,9 @@ public interface Recipe {
@Nullable LocalDateTime getModified();
String getSlug();
String getTitle();
@Nullable Integer getPreparationTime();
@Nullable Integer getCookingTime();
@Nullable Integer getTotalTime();
String getRawText();
User getOwner();
Set<RecipeStar> getStars();

View File

@ -23,7 +23,7 @@ public final class RecipeEntity implements Recipe {
private Long id;
@Column(nullable = false)
private LocalDateTime created = LocalDateTime.now();
private LocalDateTime created;
private LocalDateTime modified;
@ -33,6 +33,15 @@ public final class RecipeEntity implements Recipe {
@Column(nullable = false)
private String title;
@Nullable
private Integer preparationTime;
@Nullable
private Integer cookingTime;
@Nullable
private Integer totalTime;
@Lob
@Column(name = "raw_text", columnDefinition = "TEXT", nullable = false)
@Basic(fetch = FetchType.LAZY)
@ -108,6 +117,33 @@ public final class RecipeEntity implements Recipe {
this.title = title;
}
@Override
public @Nullable Integer getPreparationTime() {
return this.preparationTime;
}
public void setPreparationTime(@Nullable Integer preparationTime) {
this.preparationTime = preparationTime;
}
@Override
public @Nullable Integer getCookingTime() {
return this.cookingTime;
}
public void setCookingTime(@Nullable Integer cookingTime) {
this.cookingTime = cookingTime;
}
@Override
public @Nullable Integer getTotalTime() {
return this.totalTime;
}
public void setTotalTime(@Nullable Integer totalTime) {
this.totalTime = totalTime;
}
@Override
public String getRawText() {
return this.rawText;

View File

@ -1,16 +1,18 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.image.ImageService;
import app.mealsmadeeasy.api.image.S3ImageEntity;
import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
@ -99,22 +101,31 @@ public class RecipeServiceImpl implements RecipeService {
return this.recipeRepository.getViewerCount(recipeId);
}
private FullRecipeView getFullView(RecipeEntity recipe) {
final FullRecipeView view = new FullRecipeView();
view.setId(recipe.getId());
view.setCreated(recipe.getCreated());
view.setModified(recipe.getModified());
view.setSlug(recipe.getSlug());
view.setTitle(recipe.getTitle());
view.setText(this.getRenderedMarkdown(recipe));
view.setOwner(UserInfoView.from(recipe.getOwner()));
view.setStarCount(this.getStarCount(recipe));
view.setViewerCount(this.getViewerCount(recipe.getId()));
if (recipe.getMainImage() != null) {
view.setMainImage(this.imageService.toImageView(recipe.getMainImage()));
@Contract("null, _ -> null")
private @Nullable ImageView getImageView(@Nullable Image image, @Nullable User viewer) {
if (image != null) {
return this.imageService.toImageView(image, viewer);
} else {
return null;
}
view.setIsPublic(recipe.isPublic());
return view;
}
private FullRecipeView getFullView(RecipeEntity recipe, @Nullable User viewer) {
return FullRecipeView.from(
recipe,
this.getRenderedMarkdown(recipe),
this.getStarCount(recipe),
this.getViewerCount(recipe.getId()),
this.getImageView(recipe.getMainImage(), viewer)
);
}
private RecipeInfoView getInfoView(RecipeEntity recipe, @Nullable User viewer) {
return RecipeInfoView.from(
recipe,
this.getStarCount(recipe),
this.getImageView(recipe.getMainImage(), viewer)
);
}
@Override
@ -123,7 +134,7 @@ public class RecipeServiceImpl implements RecipeService {
final RecipeEntity recipe = this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, "No such Recipe for id: " + id
));
return this.getFullView(recipe);
return this.getFullView(recipe, viewer);
}
@Override
@ -134,29 +145,14 @@ public class RecipeServiceImpl implements RecipeService {
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
"No such Recipe for username " + username + " and slug: " + slug
));
return this.getFullView(recipe);
return this.getFullView(recipe, viewer);
}
@Override
public Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer) {
return this.recipeRepository.findAllViewableBy((UserEntity) viewer, pageable).map(entity -> {
final RecipeInfoView view = new RecipeInfoView();
view.setId(entity.getId());
if (entity.getModified() != null) {
view.setUpdated(entity.getModified());
} else {
view.setUpdated(entity.getCreated());
}
view.setSlug(entity.getSlug());
view.setTitle(entity.getTitle());
view.setOwner(UserInfoView.from(entity.getOwner()));
view.setIsPublic(entity.isPublic());
view.setStarCount(this.getStarCount(entity));
if (entity.getMainImage() != null) {
view.setMainImage(this.imageService.toImageView(entity.getMainImage()));
}
return view;
});
return this.recipeRepository.findAllViewableBy((UserEntity) viewer, pageable).map(recipe ->
this.getInfoView(recipe, viewer)
);
}
@Override
@ -194,6 +190,18 @@ public class RecipeServiceImpl implements RecipeService {
entity.setTitle(spec.getTitle());
didModify = true;
}
if (spec.getPreparationTime() != null) {
entity.setPreparationTime(spec.getPreparationTime());
didModify = true;
}
if (spec.getCookingTime() != null) {
entity.setCookingTime(spec.getCookingTime());
didModify = true;
}
if (spec.getTotalTime() != null) {
entity.setTotalTime(spec.getTotalTime());
didModify = true;
}
if (spec.getRawText() != null) {
entity.setRawText(spec.getRawText());
didModify = true;

View File

@ -7,6 +7,9 @@ public class RecipeCreateSpec {
private String slug;
private String title;
private @Nullable Integer preparationTime;
private @Nullable Integer cookingTime;
private @Nullable Integer totalTime;
private String rawText;
private boolean isPublic;
private @Nullable Image mainImage;
@ -27,6 +30,30 @@ public class RecipeCreateSpec {
this.title = title;
}
public @Nullable Integer getPreparationTime() {
return this.preparationTime;
}
public void setPreparationTime(@Nullable Integer preparationTime) {
this.preparationTime = preparationTime;
}
public @Nullable Integer getCookingTime() {
return this.cookingTime;
}
public void setCookingTime(@Nullable Integer cookingTime) {
this.cookingTime = cookingTime;
}
public @Nullable Integer getTotalTime() {
return this.totalTime;
}
public void setTotalTime(@Nullable Integer totalTime) {
this.totalTime = totalTime;
}
public String getRawText() {
return this.rawText;
}

View File

@ -7,6 +7,9 @@ public class RecipeUpdateSpec {
private @Nullable String slug;
private @Nullable String title;
private @Nullable Integer preparationTime;
private @Nullable Integer cookingTime;
private @Nullable Integer totalTime;
private @Nullable String rawText;
private @Nullable Boolean isPublic;
private @Nullable Image mainImage;
@ -27,6 +30,30 @@ public class RecipeUpdateSpec {
this.title = title;
}
public @Nullable Integer getPreparationTime() {
return this.preparationTime;
}
public void setPreparationTime(@Nullable Integer preparationTime) {
this.preparationTime = preparationTime;
}
public @Nullable Integer getCookingTime() {
return this.cookingTime;
}
public void setCookingTime(@Nullable Integer cookingTime) {
this.cookingTime = cookingTime;
}
public @Nullable Integer getTotalTime() {
return this.totalTime;
}
public void setTotalTime(@Nullable Integer totalTime) {
this.totalTime = totalTime;
}
public @Nullable String getRawText() {
return this.rawText;
}

View File

@ -1,6 +1,7 @@
package app.mealsmadeeasy.api.recipe.view;
import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable;
@ -8,11 +9,39 @@ import java.time.LocalDateTime;
public class FullRecipeView {
public static FullRecipeView from(
Recipe recipe,
String renderedText,
int starCount,
int viewerCount,
ImageView mainImage
) {
final FullRecipeView view = new FullRecipeView();
view.setId(recipe.getId());
view.setCreated(recipe.getCreated());
view.setModified(recipe.getModified());
view.setSlug(recipe.getSlug());
view.setTitle(recipe.getTitle());
view.setPreparationTime(recipe.getPreparationTime());
view.setCookingTime(recipe.getCookingTime());
view.setTotalTime(recipe.getTotalTime());
view.setText(renderedText);
view.setOwner(UserInfoView.from(recipe.getOwner()));
view.setStarCount(starCount);
view.setViewerCount(viewerCount);
view.setMainImage(mainImage);
view.setIsPublic(recipe.isPublic());
return view;
}
private long id;
private LocalDateTime created;
private @Nullable LocalDateTime modified;
private String slug;
private String title;
private @Nullable Integer preparationTime;
private @Nullable Integer cookingTime;
private @Nullable Integer totalTime;
private String text;
private UserInfoView owner;
private int starCount;
@ -60,6 +89,30 @@ public class FullRecipeView {
this.title = title;
}
public @Nullable Integer getPreparationTime() {
return this.preparationTime;
}
public void setPreparationTime(@Nullable Integer preparationTime) {
this.preparationTime = preparationTime;
}
public @Nullable Integer getCookingTime() {
return this.cookingTime;
}
public void setCookingTime(@Nullable Integer cookingTime) {
this.cookingTime = cookingTime;
}
public @Nullable Integer getTotalTime() {
return this.totalTime;
}
public void setTotalTime(@Nullable Integer totalTime) {
this.totalTime = totalTime;
}
public @Nullable String getText() {
return this.text;
}

View File

@ -1,6 +1,7 @@
package app.mealsmadeeasy.api.recipe.view;
import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable;
@ -8,10 +9,31 @@ import java.time.LocalDateTime;
public final class RecipeInfoView {
public static RecipeInfoView from(Recipe recipe, int starCount, @Nullable ImageView mainImage) {
final RecipeInfoView view = new RecipeInfoView();
view.setId(recipe.getId());
view.setCreated(recipe.getCreated());
view.setModified(recipe.getModified());
view.setSlug(recipe.getSlug());
view.setTitle(recipe.getTitle());
view.setPreparationTime(recipe.getPreparationTime());
view.setCookingTime(recipe.getCookingTime());
view.setTotalTime(recipe.getTotalTime());
view.setOwner(UserInfoView.from(recipe.getOwner()));
view.setIsPublic(recipe.isPublic());
view.setStarCount(starCount);
view.setMainImage(mainImage);
return view;
}
private long id;
private LocalDateTime updated;
private LocalDateTime created;
private LocalDateTime modified;
private String slug;
private String title;
private @Nullable Integer preparationTime;
private @Nullable Integer cookingTime;
private @Nullable Integer totalTime;
private UserInfoView owner;
private boolean isPublic;
private int starCount;
@ -25,12 +47,20 @@ public final class RecipeInfoView {
this.id = id;
}
public LocalDateTime getUpdated() {
return this.updated;
public LocalDateTime getCreated() {
return this.created;
}
public void setUpdated(LocalDateTime updated) {
this.updated = updated;
public void setCreated(LocalDateTime created) {
this.created = created;
}
public LocalDateTime getModified() {
return this.modified;
}
public void setModified(LocalDateTime modified) {
this.modified = modified;
}
public String getSlug() {
@ -49,6 +79,30 @@ public final class RecipeInfoView {
this.title = title;
}
public @Nullable Integer getPreparationTime() {
return this.preparationTime;
}
public void setPreparationTime(@Nullable Integer preparationTime) {
this.preparationTime = preparationTime;
}
public @Nullable Integer getCookingTime() {
return this.cookingTime;
}
public void setCookingTime(@Nullable Integer cookingTime) {
this.cookingTime = cookingTime;
}
public @Nullable Integer getTotalTime() {
return this.totalTime;
}
public void setTotalTime(@Nullable Integer totalTime) {
this.totalTime = totalTime;
}
public UserInfoView getOwner() {
return this.owner;
}