Resolve "Add Reviews"
This commit is contained in:
parent
275128c389
commit
36fc1fa8be
|
@ -4,6 +4,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.JoinTable;
|
||||
|
@ -17,6 +19,7 @@ import javax.persistence.Table;
|
|||
public class Category {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private int id;
|
||||
private int ordinal;
|
||||
@OneToMany(mappedBy = "category")
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
package de.mstock.monolith.domain;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import de.mstock.monolith.web.CategoryDTO;
|
||||
import de.mstock.monolith.web.ProductDTO;
|
||||
import de.mstock.monolith.web.ReviewDTO;
|
||||
|
||||
@Component
|
||||
public class DataTransferObjectFactory {
|
||||
|
||||
|
||||
/**
|
||||
* Creates a Data Transfer Object (DTO).
|
||||
*
|
||||
* @param category database entity
|
||||
* @param locale the requested locale
|
||||
* @return DTO
|
||||
*/
|
||||
public CategoryDTO createCategoryDTO(Category category, Locale locale) {
|
||||
CategoryI18n i18n = category.getI18n().get(locale.getLanguage());
|
||||
CategoryDTO categoryDTO = new CategoryDTO();
|
||||
categoryDTO.setName(i18n.getName());
|
||||
categoryDTO.setPrettyUrlFragment(i18n.getPrettyUrlFragment());
|
||||
return categoryDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Data Transfer Object (DTO).
|
||||
*
|
||||
* @param product database entity
|
||||
* @param locale the requested locale
|
||||
* @return DTO
|
||||
*/
|
||||
public ProductDTO createProductDTO(Product product, Locale locale) {
|
||||
return createProductDTO(product, locale, NumberFormat.getCurrencyInstance(locale));
|
||||
}
|
||||
|
||||
private ProductDTO createProductDTO(Product product, Locale locale, NumberFormat numberFormat) {
|
||||
ProductDTO productDTO = createProductWithoutReviewsDTO(product, locale, numberFormat);
|
||||
ProductI18n i18n = product.getI18n().get(locale.getLanguage());
|
||||
productDTO.setReviews(createReviewDTOs(i18n.getReviews()));
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Data Transfer Objects (DTOs).
|
||||
*
|
||||
* @param products database entities
|
||||
* @param locale the requested locale
|
||||
* @return DTOs
|
||||
*/
|
||||
public List<ProductDTO> createProductDTOs(List<Product> products, Locale locale) {
|
||||
List<ProductDTO> productDTOs = new ArrayList<>(products.size());
|
||||
NumberFormat numberFormat = NumberFormat.getCurrencyInstance(locale);
|
||||
for (Product product : products) {
|
||||
productDTOs.add(createProductDTO(product, locale, numberFormat));
|
||||
}
|
||||
return productDTOs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Data Transfer Objects (DTOs) without loading their reviews.
|
||||
*
|
||||
* @param products database entities
|
||||
* @param locale the requested locale
|
||||
* @return DTOs
|
||||
*/
|
||||
public List<ProductDTO> createProductWithoutReviewsDTOs(List<Product> products, Locale locale) {
|
||||
List<ProductDTO> productDTOs = new ArrayList<>(products.size());
|
||||
NumberFormat numberFormat = NumberFormat.getCurrencyInstance(locale);
|
||||
for (Product product : products) {
|
||||
productDTOs.add(createProductWithoutReviewsDTO(product, locale, numberFormat));
|
||||
}
|
||||
return productDTOs;
|
||||
}
|
||||
|
||||
private ProductDTO createProductWithoutReviewsDTO(Product product, Locale locale,
|
||||
NumberFormat numberFormat) {
|
||||
ProductI18n i18n = product.getI18n().get(locale.getLanguage());
|
||||
String price = numberFormat.format(i18n.getPrice());
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
productDTO.setItemNumber(product.getItemNumber());
|
||||
productDTO.setUnit(product.getUnit());
|
||||
productDTO.setName(i18n.getName());
|
||||
productDTO.setPrettyUrlFragment(i18n.getPrettyUrlFragment());
|
||||
productDTO.setPrice(price);
|
||||
productDTO.setDescription(i18n.getDescription());
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Data Transfer Object (DTO).
|
||||
*
|
||||
* @param review database entity
|
||||
* @return DTO
|
||||
*/
|
||||
public ReviewDTO createReviewDTO(Review review) {
|
||||
ReviewDTO dto = new ReviewDTO();
|
||||
dto.setLanguage(review.getLocaleLanguage());
|
||||
dto.setRatingStars(review.getRatingStars());
|
||||
dto.setFirstName(review.getFirstName());
|
||||
dto.setLastName(review.getLastName());
|
||||
dto.setText(review.getText());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private List<ReviewDTO> createReviewDTOs(List<Review> reviews) {
|
||||
List<ReviewDTO> ratingDTOs = new ArrayList<>(reviews.size());
|
||||
for (Review review : reviews) {
|
||||
ratingDTOs.add(createReviewDTO(review));
|
||||
}
|
||||
return Collections.unmodifiableList(ratingDTOs);
|
||||
}
|
||||
|
||||
}
|
|
@ -5,6 +5,8 @@ import java.util.Map;
|
|||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.MapKey;
|
||||
import javax.persistence.OneToMany;
|
||||
|
@ -15,6 +17,7 @@ import javax.persistence.Table;
|
|||
public class Product {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private int id;
|
||||
private String itemNumber;
|
||||
@Enumerated(EnumType.ORDINAL)
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package de.mstock.monolith.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import javax.persistence.EmbeddedId;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.JoinColumns;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.MapsId;
|
||||
import javax.persistence.OneToMany;
|
||||
import javax.persistence.Table;
|
||||
|
||||
@Entity
|
||||
|
@ -21,6 +25,9 @@ public class ProductI18n {
|
|||
private String prettyUrlFragment;
|
||||
private BigDecimal price;
|
||||
private String description;
|
||||
@OneToMany
|
||||
@JoinColumns({@JoinColumn(name = "localeLanguage"), @JoinColumn(name = "productId")})
|
||||
private List<Review> reviews;
|
||||
|
||||
public ProductI18nPk getProductI18nPk() {
|
||||
return productI18nPk;
|
||||
|
@ -61,4 +68,12 @@ public class ProductI18n {
|
|||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public List<Review> getReviews() {
|
||||
return reviews;
|
||||
}
|
||||
|
||||
public void setReviews(List<Review> reviews) {
|
||||
this.reviews = reviews;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import org.springframework.data.repository.Repository;
|
|||
|
||||
public interface ProductRepository extends Repository<Product, Integer> {
|
||||
|
||||
@Query("select p from Product p join fetch p.i18n i "
|
||||
@Query("select p from Product p join fetch p.i18n i left join fetch i.reviews r "
|
||||
+ "where key(i) = ?1 and lower(i.name) = ?2")
|
||||
Product findByI18nName(String language, String name);
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package de.mstock.monolith.domain;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "reviews")
|
||||
public class Review {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private int id;
|
||||
private int productId;
|
||||
private String localeLanguage;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private int ratingStars;
|
||||
private String text;
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getProductId() {
|
||||
return productId;
|
||||
}
|
||||
|
||||
public void setProductId(int productId) {
|
||||
this.productId = productId;
|
||||
}
|
||||
|
||||
public String getLocaleLanguage() {
|
||||
return localeLanguage;
|
||||
}
|
||||
|
||||
public void setLocaleLanguage(String localeLanguage) {
|
||||
this.localeLanguage = localeLanguage;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public int getRatingStars() {
|
||||
return ratingStars;
|
||||
}
|
||||
|
||||
public void setRatingStars(int ratingStars) {
|
||||
this.ratingStars = ratingStars;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package de.mstock.monolith.domain;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface ReviewRepository extends CrudRepository<Review, Integer> {
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package de.mstock.monolith.service;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import de.mstock.monolith.domain.DataTransferObjectFactory;
|
||||
import de.mstock.monolith.domain.Product;
|
||||
import de.mstock.monolith.domain.ProductRepository;
|
||||
import de.mstock.monolith.domain.Review;
|
||||
import de.mstock.monolith.domain.ReviewRepository;
|
||||
import de.mstock.monolith.web.ReviewDTO;
|
||||
import de.mstock.monolith.web.form.ReviewForm;
|
||||
|
||||
@Service
|
||||
public class ReviewService {
|
||||
|
||||
@Autowired
|
||||
private ReviewRepository reviewRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
private DataTransferObjectFactory dtoFactory;
|
||||
|
||||
/**
|
||||
* Stores a review from a posted form.
|
||||
*
|
||||
* @param reviewForm Post data
|
||||
* @param locale Language context
|
||||
* @param prettyUrlFragment Used to get the product context
|
||||
* @return DTO
|
||||
*/
|
||||
public ReviewDTO saveReview(ReviewForm reviewForm, Locale locale, String prettyUrlFragment) {
|
||||
Product product = productRepository.findByI18nName(locale.getLanguage(), prettyUrlFragment);
|
||||
Review review = new Review();
|
||||
review.setProductId(product.getId());
|
||||
review.setLocaleLanguage(locale.getLanguage());
|
||||
review.setFirstName(reviewForm.getFirstName());
|
||||
review.setLastName(reviewForm.getLastName());
|
||||
review.setRatingStars(reviewForm.getRatingStars());
|
||||
review.setText(reviewForm.getText());
|
||||
return dtoFactory.createReviewDTO(reviewRepository.save(review));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package de.mstock.monolith.service;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -10,10 +9,9 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||
import org.springframework.stereotype.Service;
|
||||
|
||||
import de.mstock.monolith.domain.Category;
|
||||
import de.mstock.monolith.domain.CategoryI18n;
|
||||
import de.mstock.monolith.domain.CategoryRepository;
|
||||
import de.mstock.monolith.domain.DataTransferObjectFactory;
|
||||
import de.mstock.monolith.domain.Product;
|
||||
import de.mstock.monolith.domain.ProductI18n;
|
||||
import de.mstock.monolith.domain.ProductRepository;
|
||||
import de.mstock.monolith.web.CategoryDTO;
|
||||
import de.mstock.monolith.web.ProductDTO;
|
||||
|
@ -27,6 +25,9 @@ public class ShopService {
|
|||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
private DataTransferObjectFactory dtoFactory;
|
||||
|
||||
/**
|
||||
* Gets all categories of the current language.
|
||||
*
|
||||
|
@ -36,8 +37,7 @@ public class ShopService {
|
|||
String language = locale.getLanguage();
|
||||
List<CategoryDTO> categories = new ArrayList<>();
|
||||
for (Category category : categoryRepository.findAllOrdered(language)) {
|
||||
CategoryI18n i18n = category.getI18n().get(language);
|
||||
categories.add(new CategoryDTO(i18n.getName(), i18n.getPrettyUrlFragment()));
|
||||
categories.add(dtoFactory.createCategoryDTO(category, locale));
|
||||
}
|
||||
return Collections.unmodifiableList(categories);
|
||||
}
|
||||
|
@ -53,33 +53,22 @@ public class ShopService {
|
|||
if (category == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
List<ProductDTO> products = new ArrayList<>();
|
||||
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
|
||||
for (Product product : category.getProducts()) {
|
||||
ProductI18n i18n = product.getI18n().get(language);
|
||||
String price = currencyFormat.format(i18n.getPrice());
|
||||
products.add(new ProductDTO(product.getItemNumber(), product.getUnit(), i18n.getName(),
|
||||
i18n.getPrettyUrlFragment(), price, i18n.getDescription()));
|
||||
}
|
||||
List<ProductDTO> products =
|
||||
dtoFactory.createProductWithoutReviewsDTOs(category.getProducts(), locale);
|
||||
return Collections.unmodifiableList(products);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a products in the current language.
|
||||
* Gets a product in the current language.
|
||||
*
|
||||
* @return A simplified Data Transfer Object.
|
||||
*/
|
||||
public ProductDTO getProduct(Locale locale, String prettyUrlFragment) {
|
||||
String language = locale.getLanguage();
|
||||
Product product = productRepository.findByI18nName(language, prettyUrlFragment);
|
||||
Product product = productRepository.findByI18nName(locale.getLanguage(), prettyUrlFragment);
|
||||
if (product == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
ProductI18n i18n = product.getI18n().get(language);
|
||||
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
|
||||
String price = currencyFormat.format(i18n.getPrice());
|
||||
return new ProductDTO(product.getItemNumber(), product.getUnit(), i18n.getName(),
|
||||
i18n.getPrettyUrlFragment(), price, i18n.getDescription());
|
||||
return dtoFactory.createProductDTO(product, locale);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,20 +2,23 @@ package de.mstock.monolith.web;
|
|||
|
||||
public class CategoryDTO {
|
||||
|
||||
private final String name;
|
||||
private final String prettyUrlFragment;
|
||||
|
||||
public CategoryDTO(String name, String prettyUrlFragment) {
|
||||
this.name = name;
|
||||
this.prettyUrlFragment = prettyUrlFragment;
|
||||
}
|
||||
private String name;
|
||||
private String prettyUrlFragment;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getPrettyUrlFragment() {
|
||||
return prettyUrlFragment;
|
||||
}
|
||||
|
||||
public void setPrettyUrlFragment(String prettyUrlFragment) {
|
||||
this.prettyUrlFragment = prettyUrlFragment;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,14 +2,19 @@ package de.mstock.monolith.web;
|
|||
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import de.mstock.monolith.service.ReviewService;
|
||||
import de.mstock.monolith.service.ShopService;
|
||||
import de.mstock.monolith.web.form.ReviewForm;
|
||||
|
||||
@Controller
|
||||
public class ProductController {
|
||||
|
@ -19,6 +24,9 @@ public class ProductController {
|
|||
@Autowired
|
||||
private ShopService shopService;
|
||||
|
||||
@Autowired
|
||||
private ReviewService reviewService;
|
||||
|
||||
/**
|
||||
* Product page
|
||||
*
|
||||
|
@ -28,7 +36,32 @@ public class ProductController {
|
|||
* @return The template's name.
|
||||
*/
|
||||
@RequestMapping(value = "/products/{prettyUrlFragment:[\\w-]+}", method = RequestMethod.GET)
|
||||
public String homepage(@PathVariable String prettyUrlFragment, Model model, Locale locale) {
|
||||
public String product(@PathVariable String prettyUrlFragment, Model model, Locale locale) {
|
||||
model.addAttribute("categories", shopService.getCategories(locale));
|
||||
model.addAttribute("product", shopService.getProduct(locale, prettyUrlFragment));
|
||||
return TEMPLATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post a review
|
||||
*
|
||||
* @param reviewForm Form data
|
||||
* @param bindingResult Form binding result after validation
|
||||
* @param prettyUrlFragment Product context
|
||||
* @param model Template model
|
||||
* @param locale Language context
|
||||
* @return The template's name.
|
||||
*/
|
||||
@RequestMapping(value = "/products/{prettyUrlFragment:[\\w-]+}", method = RequestMethod.POST)
|
||||
public String post(@Valid ReviewForm reviewForm, BindingResult bindingResult,
|
||||
@PathVariable String prettyUrlFragment, Model model, Locale locale) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
model.addAttribute("success", false);
|
||||
} else {
|
||||
model.addAttribute("success", true);
|
||||
model.addAttribute("reviewPost",
|
||||
reviewService.saveReview(reviewForm, locale, prettyUrlFragment));
|
||||
}
|
||||
model.addAttribute("categories", shopService.getCategories(locale));
|
||||
model.addAttribute("product", shopService.getProduct(locale, prettyUrlFragment));
|
||||
return TEMPLATE;
|
||||
|
|
|
@ -1,57 +1,73 @@
|
|||
package de.mstock.monolith.web;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.mstock.monolith.domain.ProductWeightUnit;
|
||||
|
||||
public class ProductDTO {
|
||||
|
||||
private final String itemNumber;
|
||||
private final ProductWeightUnit unit;
|
||||
private final String name;
|
||||
private final String prettyUrlFragment;
|
||||
private final String price;
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* A simplified representation of a product.
|
||||
*
|
||||
* @param itemNumber A product's unique item number.
|
||||
* @param unit The unit of a product that relates to its price.
|
||||
* @param name The localized name.
|
||||
* @param price The product's price per unit.
|
||||
* @param description The description of the product.
|
||||
*/
|
||||
public ProductDTO(String itemNumber, ProductWeightUnit unit, String name,
|
||||
String prettyUrlFragment, String price, String description) {
|
||||
this.itemNumber = itemNumber;
|
||||
this.unit = unit;
|
||||
this.name = name;
|
||||
this.prettyUrlFragment = prettyUrlFragment;
|
||||
this.price = price;
|
||||
this.description = description;
|
||||
}
|
||||
private String itemNumber;
|
||||
private ProductWeightUnit unit;
|
||||
private String name;
|
||||
private String prettyUrlFragment;
|
||||
private String price;
|
||||
private String description;
|
||||
private List<ReviewDTO> reviews;
|
||||
|
||||
public String getItemNumber() {
|
||||
return itemNumber;
|
||||
}
|
||||
|
||||
public void setItemNumber(String itemNumber) {
|
||||
this.itemNumber = itemNumber;
|
||||
}
|
||||
|
||||
public ProductWeightUnit getUnit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
public void setUnit(ProductWeightUnit unit) {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getPrettyUrlFragment() {
|
||||
return prettyUrlFragment;
|
||||
}
|
||||
|
||||
public void setPrettyUrlFragment(String prettyUrlFragment) {
|
||||
this.prettyUrlFragment = prettyUrlFragment;
|
||||
}
|
||||
|
||||
public String getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(String price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public List<ReviewDTO> getReviews() {
|
||||
return reviews;
|
||||
}
|
||||
|
||||
public void setReviews(List<ReviewDTO> reviews) {
|
||||
this.reviews = reviews;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package de.mstock.monolith.web;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public class ReviewDTO {
|
||||
|
||||
private Locale locale;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private int ratingStars;
|
||||
private String text;
|
||||
|
||||
public String getLanguage() {
|
||||
return locale.getLanguage();
|
||||
}
|
||||
|
||||
public void setLanguage(String language) {
|
||||
this.locale = new Locale(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Presentation layer can use this method to access the full name of the reviewer.
|
||||
*
|
||||
* @return concatenated name
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
if (StringUtils.isNoneBlank(firstName, lastName)) {
|
||||
return firstName + " " + lastName.charAt(0) + ".";
|
||||
}
|
||||
return StringUtils.defaultString(firstName);
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public int getRatingStars() {
|
||||
return ratingStars;
|
||||
}
|
||||
|
||||
public void setRatingStars(int ratingStars) {
|
||||
this.ratingStars = ratingStars;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package de.mstock.monolith.web.form;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
public class ReviewForm {
|
||||
|
||||
@Size(max = 80)
|
||||
private String firstName;
|
||||
@Size(max = 80)
|
||||
private String lastName;
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
@NotNull
|
||||
private int ratingStars;
|
||||
@Size(max = 1000)
|
||||
private String text;
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public int getRatingStars() {
|
||||
return ratingStars;
|
||||
}
|
||||
|
||||
public void setRatingStars(int ratingStars) {
|
||||
this.ratingStars = ratingStars;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
alter table categories_i18n
|
||||
add constraint categories_i18n_category_id_fkey foreign key (category_id)
|
||||
references categories
|
||||
on delete cascade;
|
||||
|
||||
alter table products_i18n
|
||||
add constraint products_i18n_product_id_fkey foreign key (product_id)
|
||||
references products
|
||||
on delete cascade;
|
|
@ -0,0 +1,18 @@
|
|||
create table reviews (
|
||||
id serial primary key,
|
||||
product_id integer,
|
||||
locale_language varchar(2) not null,
|
||||
first_name varchar(80),
|
||||
last_name varchar(80),
|
||||
rating_stars integer not null check (rating_stars between 1 and 5),
|
||||
text varchar(1000),
|
||||
foreign key (product_id, locale_language) references products_i18n(product_id, locale_language)
|
||||
);
|
||||
|
||||
insert into reviews (id, product_id, locale_language, first_name, last_name, rating_stars, text) values
|
||||
(1, 1, 'en', 'John', 'Doe', 5, 'Absolutely perfect!'),
|
||||
(2, 1, 'de', 'Max', 'Mustermann', 3, null),
|
||||
(3, 1, 'de', 'Erika', 'Mustermann', 4, 'Ich liebe dieses Produkt! Leider ist ab und zu auch eine matschige Kiwi dabei.'),
|
||||
(4, 4, 'de', 'Otto', 'Normalverbraucher', 4, 'Genau das was ich gesucht habe!'),
|
||||
(5, 5, 'de', null, null, 1, 'Schmeckt nicht!'),
|
||||
(6, 5, 'en', 'John', null, 2, 'What''s the country of origin of these tomatoes?');
|
|
@ -0,0 +1,3 @@
|
|||
alter sequence categories_id_seq restart with 100;
|
||||
alter sequence products_id_seq restart with 100;
|
||||
alter sequence reviews_id_seq restart with 100;
|
|
@ -15,3 +15,6 @@ features.headline2 = Das ist gut.
|
|||
features.subheadline2 = Sieh' selbst.
|
||||
features.headline3 = Und zum Schluss, das hier.
|
||||
features.subheadline3 = Bingo.
|
||||
reviews.title = Rezensionen
|
||||
validation.review.success = Danke für deine Rezension!
|
||||
validation.error = Es ist ein Fehler aufgetreten!
|
||||
|
|
|
@ -15,3 +15,6 @@ features.headline2 = Oh yeah, it's that good.
|
|||
features.subheadline2 = See for yourself.
|
||||
features.headline3 = And lastly, this one.
|
||||
features.subheadline3 = Checkmate.
|
||||
reviews.title = Reviews
|
||||
validation.review.success = Thanks for your review!
|
||||
validation.error = An error occured!
|
||||
|
|
|
@ -11,15 +11,66 @@
|
|||
<div class="container">
|
||||
<div class="row reviews" th:fragment="reviews">
|
||||
<div class="col-md-12">
|
||||
<h2 th:inline="text">Reviews</h2>
|
||||
<div class="review">
|
||||
<h2 th:text="#{reviews.title}">Reviews</h2>
|
||||
<div th:if="${success != null and success}" class="alert alert-success" role="alert">
|
||||
<p th:text="#{validation.review.success}">Thanks for your review!</p>
|
||||
<p>
|
||||
<span class="rating"></span> Great product!
|
||||
<span class="rating" th:switch="${reviewPost.ratingStars}">
|
||||
<th:block th:case="1">★</th:block>
|
||||
<th:block th:case="2">★★</th:block>
|
||||
<th:block th:case="3">★★★</th:block>
|
||||
<th:block th:case="4">★★★★</th:block>
|
||||
<th:block th:case="5">★★★★★</th:block>
|
||||
</span> <span th:text="${reviewPost.displayName}">John D.</span>
|
||||
</p>
|
||||
<p>John D.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consetetur sadipscing
|
||||
elitr, sed diam nonumy eirmod tempor invidunt ut labore et
|
||||
dolore magna aliquyam.</p>
|
||||
<p th:text="${reviewPost.text}">Lorem ipsum</p>
|
||||
</div>
|
||||
<div th:if="${success != null and !success}" th:text="#{validation.error}" class="alert alert-danger" role="alert">An error occured!</div>
|
||||
<form th:if="${success == null}" class="form-horizontal" action="#" th:action="@{/products/__${product.prettyUrlFragment}__.html}" th:object="${reviewPost}" method="post">
|
||||
<div class="form-group">
|
||||
<label for="firstName" class="col-sm-2 control-label">First Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="firstName" name="firstName" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="lastName" class="col-sm-2 control-label">Last Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="lastName" name="lastName" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Rating</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="radio" id="ratingStars1" name="ratingStars" value="1" checked="checked" /> <label for="ratingStars1">★</label>
|
||||
<input type="radio" id="ratingStars2" name="ratingStars" value="2" /> <label for="ratingStars2">★★</label>
|
||||
<input type="radio" id="ratingStars3" name="ratingStars" value="3" /> <label for="ratingStars3">★★★</label>
|
||||
<input type="radio" id="ratingStars4" name="ratingStars" value="4" /> <label for="ratingStars4">★★★★</label>
|
||||
<input type="radio" id="ratingStars5" name="ratingStars" value="5" /> <label for="ratingStars5">★★★★★</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="text" class="col-sm-2 control-label">Text</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="text" name="text"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
||||
<hr />
|
||||
<div class="review" th:each="review : ${product.reviews}">
|
||||
<p>
|
||||
<span class="rating" th:switch="${review.ratingStars}">
|
||||
<th:block th:case="1">★</th:block>
|
||||
<th:block th:case="2">★★</th:block>
|
||||
<th:block th:case="3">★★★</th:block>
|
||||
<th:block th:case="4">★★★★</th:block>
|
||||
<th:block th:case="5">★★★★★</th:block>
|
||||
</span> <span th:text="${review.displayName}">John D.</span>
|
||||
</p>
|
||||
<p th:text="${review.text}">Lorem ipsum dolor sit amet,
|
||||
consetetur sadipscing elitr, sed diam nonumy eirmod tempor
|
||||
invidunt ut labore et dolore magna aliquyam.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package de.mstock.monolith.domain;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import de.mstock.monolith.web.ProductDTO;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class DataTransferObjectFactoryTest {
|
||||
|
||||
@InjectMocks
|
||||
private DataTransferObjectFactory dtoFactory;
|
||||
|
||||
@Test
|
||||
public void shouldCreateProductDTOWithoutReviews() {
|
||||
Product product = mock(Product.class, RETURNS_DEEP_STUBS);
|
||||
ProductI18n productI18n = mock(ProductI18n.class);
|
||||
when(product.getI18n().get(any())).thenReturn(productI18n);
|
||||
when(productI18n.getPrice()).thenReturn(BigDecimal.valueOf(1.23));
|
||||
verify(productI18n, never()).getReviews();
|
||||
List<ProductDTO> productDTOs =
|
||||
dtoFactory.createProductWithoutReviewsDTOs(Arrays.asList(product), new Locale("de"));
|
||||
assertThat("The review list is not present", productDTOs.get(0).getReviews(), is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateProductDTOWithReviews() {
|
||||
Product product = mock(Product.class, RETURNS_DEEP_STUBS);
|
||||
Review review = mock(Review.class);
|
||||
when(review.getLocaleLanguage()).thenReturn("de");
|
||||
when(product.getI18n().get(any()).getPrice()).thenReturn(BigDecimal.valueOf(1.23));
|
||||
when(product.getI18n().get(any()).getReviews()).thenReturn(Arrays.asList(review));
|
||||
List<ProductDTO> productDTOs =
|
||||
dtoFactory.createProductDTOs(Arrays.asList(product), new Locale("de"));
|
||||
assertThat("The review list is filled", productDTOs.get(0).getReviews(), is(not(empty())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateProductDTOWithEmptyReviews() {
|
||||
Product product = mock(Product.class, RETURNS_DEEP_STUBS);
|
||||
when(product.getI18n().get(any()).getPrice()).thenReturn(BigDecimal.valueOf(1.23));
|
||||
when(product.getI18n().get(any()).getReviews()).thenReturn(Collections.emptyList());
|
||||
List<ProductDTO> productDTOs =
|
||||
dtoFactory.createProductDTOs(Arrays.asList(product), new Locale("de"));
|
||||
assertThat("The review list is present, but empty", productDTOs.get(0).getReviews(),
|
||||
is(empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFormatPrice() {
|
||||
Locale locale = new Locale("en", "US");
|
||||
Product product = mock(Product.class, RETURNS_DEEP_STUBS);
|
||||
when(product.getI18n().get(anyString()).getPrice()).thenReturn(BigDecimal.valueOf(1.47));
|
||||
ProductDTO productDTO = dtoFactory.createProductDTO(product, locale);
|
||||
assertThat("Product has a formatted price", productDTO.getPrice(), is(equalTo("$1.47")));
|
||||
}
|
||||
}
|
|
@ -3,13 +3,12 @@ package de.mstock.monolith.service;
|
|||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -22,10 +21,9 @@ import org.mockito.runners.MockitoJUnitRunner;
|
|||
|
||||
import de.mstock.monolith.domain.Category;
|
||||
import de.mstock.monolith.domain.CategoryRepository;
|
||||
import de.mstock.monolith.domain.Product;
|
||||
import de.mstock.monolith.domain.DataTransferObjectFactory;
|
||||
import de.mstock.monolith.domain.ProductRepository;
|
||||
import de.mstock.monolith.web.CategoryDTO;
|
||||
import de.mstock.monolith.web.ProductDTO;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ShopServiceTest {
|
||||
|
@ -36,6 +34,9 @@ public class ShopServiceTest {
|
|||
@Mock
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Mock
|
||||
private DataTransferObjectFactory dataTransferObjectFactory;
|
||||
|
||||
@InjectMocks
|
||||
private ShopService shopService;
|
||||
|
||||
|
@ -45,20 +46,11 @@ public class ShopServiceTest {
|
|||
Category category = mock(Category.class, RETURNS_DEEP_STUBS);
|
||||
List<Category> categoryEntities = Arrays.asList(category, category, category);
|
||||
when(categoryRepository.findAllOrdered(eq(locale.getLanguage()))).thenReturn(categoryEntities);
|
||||
when(dataTransferObjectFactory.createCategoryDTO(any(), any()))
|
||||
.thenReturn(mock(CategoryDTO.class));
|
||||
List<CategoryDTO> categories = shopService.getCategories(locale);
|
||||
assertThat("Same amount of categories", categories.size(),
|
||||
is(equalTo(categoryEntities.size())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFormatPrice() {
|
||||
Locale locale = new Locale("en", "US");
|
||||
Product product = mock(Product.class, RETURNS_DEEP_STUBS);
|
||||
when(product.getI18n().get(anyString()).getPrice()).thenReturn(BigDecimal.valueOf(1.47));
|
||||
when(productRepository.findByI18nName(eq(locale.getLanguage()), anyString()))
|
||||
.thenReturn(product);
|
||||
ProductDTO productDTO = shopService.getProduct(locale, "foo");
|
||||
assertThat("Product has a formatted price", productDTO.getPrice(), is(equalTo("$1.47")));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Reference in New Issue