mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-07 15:31:00 +08:00
version reviews
This commit is contained in:
parent
6a528b834f
commit
d8e94d807b
@ -1,5 +1,6 @@
|
||||
package me.minidigger.hangar.config;
|
||||
|
||||
import me.minidigger.hangar.db.customtypes.JSONB;
|
||||
import me.minidigger.hangar.db.customtypes.JobState;
|
||||
import me.minidigger.hangar.db.customtypes.LoggedAction;
|
||||
import me.minidigger.hangar.db.customtypes.RoleCategory;
|
||||
@ -44,6 +45,7 @@ public class JDBIConfig {
|
||||
config.registerCustomType(LoggedAction.class, "logged_action_type");
|
||||
config.registerCustomType(RoleCategory.class, "role_category");
|
||||
config.registerCustomType(JobState.class, "job_state");
|
||||
config.registerCustomType(JSONB.class, "jsonb");
|
||||
return jdbi;
|
||||
}
|
||||
|
||||
|
@ -19,14 +19,12 @@ import me.minidigger.hangar.service.VersionService;
|
||||
import me.minidigger.hangar.service.project.FlagService;
|
||||
import me.minidigger.hangar.service.project.ProjectService;
|
||||
import me.minidigger.hangar.util.AlertUtil;
|
||||
import me.minidigger.hangar.util.AlertUtil.AlertType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@ -74,7 +72,7 @@ public class ApplicationController extends HangarController {
|
||||
mv.addObject("username", user);
|
||||
List<Activity> activities = new ArrayList<>();
|
||||
activities.addAll(userService.getFlagActivity(user));
|
||||
activities.addAll(userService.getRewiewActivity(user));
|
||||
activities.addAll(userService.getReviewActivity(user));
|
||||
mv.addObject("activities", activities);
|
||||
return mv;
|
||||
}
|
||||
|
@ -1,82 +1,203 @@
|
||||
package me.minidigger.hangar.controller;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import me.minidigger.hangar.db.customtypes.JSONB;
|
||||
import me.minidigger.hangar.db.customtypes.LoggedActionType;
|
||||
import me.minidigger.hangar.db.customtypes.LoggedActionType.VersionContext;
|
||||
import me.minidigger.hangar.db.model.ProjectVersionReviewsTable;
|
||||
import me.minidigger.hangar.db.model.ProjectVersionsTable;
|
||||
import me.minidigger.hangar.model.NamedPermission;
|
||||
import me.minidigger.hangar.model.generated.ReviewState;
|
||||
import me.minidigger.hangar.model.viewhelpers.VersionData;
|
||||
import me.minidigger.hangar.model.viewhelpers.VersionReview;
|
||||
import me.minidigger.hangar.model.viewhelpers.VersionReviewMessage;
|
||||
import me.minidigger.hangar.security.annotations.GlobalPermission;
|
||||
import me.minidigger.hangar.service.ReviewService;
|
||||
import me.minidigger.hangar.service.UserActionLogService;
|
||||
import me.minidigger.hangar.service.UserService;
|
||||
import me.minidigger.hangar.service.VersionService;
|
||||
import me.minidigger.hangar.util.RouteHelper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.stereotype.Controller;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class ReviewsController extends HangarController {
|
||||
|
||||
private final VersionService versionService;
|
||||
private final ReviewService reviewService;
|
||||
private final UserActionLogService userActionLogService;
|
||||
private final UserService userService;
|
||||
private final RouteHelper routeHelper;
|
||||
|
||||
@Autowired
|
||||
public ReviewsController(VersionService versionService) {
|
||||
public ReviewsController(VersionService versionService, ReviewService reviewService, UserActionLogService userActionLogService, UserService userService, RouteHelper routeHelper) {
|
||||
this.versionService = versionService;
|
||||
this.reviewService = reviewService;
|
||||
this.userActionLogService = userActionLogService;
|
||||
this.userService = userService;
|
||||
this.routeHelper = routeHelper;
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews")
|
||||
@GetMapping("/{author}/{slug}/versions/{version}/reviews")
|
||||
public ModelAndView showReviews(@PathVariable String author, @PathVariable String slug, @PathVariable String version) {
|
||||
ModelAndView mav = new ModelAndView("users/admin/reviews");
|
||||
VersionData versionData = versionService.getVersionData(author, slug, version);
|
||||
// TODO finish controller
|
||||
mav.addObject("version", versionData);
|
||||
mav.addObject("project", versionData.getP());
|
||||
List<VersionReview> rv = reviewService.getRecentReviews(versionData.getV().getId());
|
||||
mav.addObject("reviews", rv);
|
||||
ProjectVersionReviewsTable unfinished = rv.stream().filter(review -> review.getEndedAt() == null).findFirst().orElse(null);
|
||||
mav.addObject("mostRecentUnfinishedReview", unfinished);
|
||||
return fillModel(mav);
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews/addmessage")
|
||||
public Object addMessage(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version) {
|
||||
return null; // TODO implement addMessage request controller
|
||||
@PostMapping(value = "/{author}/{slug}/versions/{version}/reviews/addmessage", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public ResponseEntity<String> addMessage(@PathVariable String author, @PathVariable String slug, @PathVariable String version, @RequestParam String content) throws JsonProcessingException {
|
||||
ProjectVersionsTable versionsTable = versionService.getVersion(author, slug, version);
|
||||
VersionReview recentReview = reviewService.getMostRecentUnfinishedReview(versionsTable.getId());
|
||||
if (recentReview == null) {
|
||||
return new ResponseEntity<>("Review", HttpStatus.OK);
|
||||
}
|
||||
if (recentReview.getUserId() == userService.getCurrentUser().getId()) {
|
||||
recentReview.addMessage(new VersionReviewMessage(content), reviewService);
|
||||
} else {
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
return new ResponseEntity<>("Review", HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews/approve")
|
||||
public Object approveReview(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version) {
|
||||
return null; // TODO implement approveReview request controller
|
||||
@PostMapping("/{author}/{slug}/versions/{version}/reviews/approve")
|
||||
public ModelAndView approveReview(@PathVariable String author, @PathVariable String slug, @PathVariable String version) {
|
||||
ProjectVersionsTable versionsTable = versionService.getVersion(author, slug, version);
|
||||
VersionReview review = reviewService.getMostRecentUnfinishedReview(versionsTable.getId());
|
||||
if (review == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
review.setEndedAt(OffsetDateTime.now());
|
||||
reviewService.update(review);
|
||||
// TODO notifications
|
||||
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("reviews.showReviews", author, slug, version));
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews/edit/{review}")
|
||||
public Object editReview(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version, @PathVariable Object review) {
|
||||
return null; // TODO implement editReview request controller
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews/edit/{review}") // Pretty sure this isn't implemented
|
||||
public ResponseEntity<String> editReview(@PathVariable String author, @PathVariable String slug, @PathVariable String version, @PathVariable("review") long reviewId, @RequestParam String content) {
|
||||
ProjectVersionsTable versionsTable = versionService.getVersion(author, slug, version);
|
||||
VersionReview review = reviewService.getById(reviewId);
|
||||
if (review == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
review.addMessage(new VersionReviewMessage(content), reviewService);
|
||||
return new ResponseEntity<>("Review" + review, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews/init")
|
||||
public Object createReview(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version) {
|
||||
return null; // TODO implement createReview request controller
|
||||
@PostMapping(value = "/{author}/{slug}/versions/{version}/reviews/init")
|
||||
public ModelAndView createReview(@PathVariable String author, @PathVariable String slug, @PathVariable String version) {
|
||||
ProjectVersionsTable versionsTable = versionService.getVersion(author, slug, version);
|
||||
ProjectVersionReviewsTable review = new ProjectVersionReviewsTable(
|
||||
versionsTable.getId(),
|
||||
userService.getCurrentUser().getId(),
|
||||
new JSONB("{}")
|
||||
);
|
||||
reviewService.insert(review);
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("reviews.showReviews", author, slug, version));
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews/reopen")
|
||||
public Object reopenReview(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version) {
|
||||
return null; // TODO implement reopenReview request controller
|
||||
public ModelAndView reopenReview(@PathVariable String author, @PathVariable String slug, @PathVariable String version) {
|
||||
ProjectVersionsTable versionsTable = versionService.getVersion(author, slug, version);
|
||||
VersionReview review = reviewService.getRecentReviews(versionsTable.getId()).stream().findFirst().orElse(null);
|
||||
if (review == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
versionsTable.setReviewState(ReviewState.UNREVIEWED);
|
||||
versionsTable.setApprovedAt(null);
|
||||
versionsTable.setReviewerId(null);
|
||||
versionService.update(versionsTable);
|
||||
review.setEndedAt(null);
|
||||
|
||||
review.addMessage(new VersionReviewMessage("Reopened the review", System.currentTimeMillis(), "start"), reviewService);
|
||||
reviewService.update(review);
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("reviews.showReviews", author, slug, version));
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews/reviewtoggle")
|
||||
public Object backlogToggle(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version) {
|
||||
return null; // TODO implement backlogToggle request controller
|
||||
public ModelAndView backlogToggle(@PathVariable String author, @PathVariable String slug, @PathVariable String version, HttpServletRequest request) {
|
||||
ProjectVersionsTable versionsTable = versionService.getVersion(author, slug, version);
|
||||
if (versionsTable.getReviewState() != ReviewState.BACKLOG && versionsTable.getReviewState() != ReviewState.UNREVIEWED) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid state for toggle backlog");
|
||||
}
|
||||
ReviewState oldState = versionsTable.getReviewState();
|
||||
ReviewState newState = oldState == ReviewState.BACKLOG ? ReviewState.UNREVIEWED : ReviewState.BACKLOG;
|
||||
versionsTable.setReviewState(newState);
|
||||
|
||||
userActionLogService.version(request, LoggedActionType.VERSION_REVIEW_STATE_CHANGED.with(VersionContext.of(versionsTable.getProjectId(), versionsTable.getId())), newState.name(), oldState.name());
|
||||
versionService.update(versionsTable);
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("reviews.showReviews", author, slug, version));
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews/stop")
|
||||
public Object stopReview(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version) {
|
||||
return null; // TODO implement stopReview request controller
|
||||
@PostMapping(value = "/{author}/{slug}/versions/{version}/reviews/stop", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public ModelAndView stopReview(@PathVariable String author, @PathVariable String slug, @PathVariable String version, @RequestParam String content) {
|
||||
ProjectVersionsTable versionsTable = versionService.getVersion(author, slug, version);
|
||||
VersionReview review = reviewService.getMostRecentUnfinishedReview(versionsTable.getId());
|
||||
if (review == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
review.setEndedAt(OffsetDateTime.now());
|
||||
review.addMessage(new VersionReviewMessage(content, System.currentTimeMillis(), "stop"), reviewService);
|
||||
reviewService.update(review);
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("reviews.showReviews", author, slug, version));
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/reviews/takeover")
|
||||
public Object takeoverReview(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version) {
|
||||
return null; // TODO implement takeoverReview request controller
|
||||
public ModelAndView takeoverReview(@PathVariable String author, @PathVariable String slug, @PathVariable String version, @RequestParam String content) {
|
||||
ProjectVersionsTable versionsTable = versionService.getVersion(author, slug, version);
|
||||
VersionReview oldReview = reviewService.getMostRecentUnfinishedReview(versionsTable.getId());
|
||||
if (oldReview == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
oldReview.addMessage(new VersionReviewMessage(content, System.currentTimeMillis(), "takeover"), reviewService);
|
||||
reviewService.update(oldReview);
|
||||
|
||||
reviewService.insert(new ProjectVersionReviewsTable(
|
||||
versionsTable.getId(),
|
||||
userService.getCurrentUser().getId(),
|
||||
new JSONB("{}")
|
||||
));
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("reviews.showReviews", author, slug, version));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,10 +11,12 @@ import me.minidigger.hangar.db.model.ProjectVersionsTable;
|
||||
import me.minidigger.hangar.model.Color;
|
||||
import me.minidigger.hangar.model.NamedPermission;
|
||||
import me.minidigger.hangar.model.Visibility;
|
||||
import me.minidigger.hangar.model.generated.ReviewState;
|
||||
import me.minidigger.hangar.model.viewhelpers.ProjectData;
|
||||
import me.minidigger.hangar.model.viewhelpers.ScopedProjectData;
|
||||
import me.minidigger.hangar.model.viewhelpers.VersionData;
|
||||
import me.minidigger.hangar.security.annotations.GlobalPermission;
|
||||
import me.minidigger.hangar.service.ReviewService;
|
||||
import me.minidigger.hangar.service.UserActionLogService;
|
||||
import me.minidigger.hangar.service.UserService;
|
||||
import me.minidigger.hangar.service.VersionService;
|
||||
@ -43,6 +45,7 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Controller
|
||||
public class VersionsController extends HangarController {
|
||||
@ -243,16 +246,30 @@ public class VersionsController extends HangarController {
|
||||
return fillModel(mav);
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/approve")
|
||||
public Object approve(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version) {
|
||||
return null; // TODO implement approve request controller
|
||||
@PostMapping("/{author}/{slug}/versions/{version}/approve")
|
||||
public ModelAndView approve(@PathVariable String author, @PathVariable String slug, @PathVariable String version, HttpServletRequest request) {
|
||||
return _approve(author, slug, version, false, request);
|
||||
}
|
||||
|
||||
@GlobalPermission(NamedPermission.REVIEWER)
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/approvePartial")
|
||||
public Object approvePartial(@PathVariable Object author, @PathVariable Object slug, @PathVariable Object version) {
|
||||
return null; // TODO implement approvePartial request controller
|
||||
@PostMapping("/{author}/{slug}/versions/{version}/approvePartial")
|
||||
public ModelAndView approvePartial(@PathVariable String author, @PathVariable String slug, @PathVariable String version, HttpServletRequest request) {
|
||||
return _approve(author, slug, version, true, request);
|
||||
}
|
||||
|
||||
private ModelAndView _approve(String author, String slug, String version, boolean partial, HttpServletRequest request) {
|
||||
ReviewState newState = partial ? ReviewState.PARTIALLY_REVIEWED : ReviewState.REVIEWED;
|
||||
ProjectVersionsTable versionsTable = versionService.getVersion(author, slug, version);
|
||||
ReviewState oldState = versionsTable.getReviewState();
|
||||
versionsTable.setReviewState(newState);
|
||||
versionsTable.setReviewerId(userService.getCurrentUser().getId());
|
||||
versionsTable.setApprovedAt(OffsetDateTime.now());
|
||||
versionService.update(versionsTable);
|
||||
userActionLogService.version(request, LoggedActionType.VERSION_REVIEW_STATE_CHANGED.with(VersionContext.of(versionsTable.getProjectId(), versionsTable.getId())), newState.name(), oldState.name());
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("versions.show", author, slug, version));
|
||||
}
|
||||
|
||||
@RequestMapping("/{author}/{slug}/versions/{version}/confirm")
|
||||
|
@ -1,5 +1,56 @@
|
||||
package me.minidigger.hangar.db.customtypes;
|
||||
|
||||
public class JSONB {
|
||||
//TODO implement JSONB type
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.postgresql.util.PGobject;
|
||||
|
||||
public class JSONB extends PGobject {
|
||||
|
||||
private transient ObjectNode json;
|
||||
|
||||
public JSONB(String value) {
|
||||
setType("jsonb");
|
||||
this.value = value;
|
||||
parseJson();
|
||||
}
|
||||
|
||||
public JSONB(ObjectNode json) {
|
||||
setType("jsonb");
|
||||
this.value = json.toString();
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
public JSONB() {
|
||||
setType("jsonb");
|
||||
}
|
||||
|
||||
public JsonNode getJson() {
|
||||
return json;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
parseJson();
|
||||
}
|
||||
|
||||
private void parseJson() {
|
||||
try {
|
||||
this.json = (ObjectNode) new ObjectMapper().readTree(value);
|
||||
} catch (JsonProcessingException | ClassCastException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.json.toPrettyString();
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,8 @@ public interface ProjectVersionDao {
|
||||
"(:now, :versionString, :dependencies, :description, :projectId, :channelId, :fileSize, :hash, :fileName, :authorId, :createForumPost)")
|
||||
ProjectVersionsTable insert(@BindBean ProjectVersionsTable projectVersionsTable);
|
||||
|
||||
@SqlUpdate("UPDATE project_versions SET visibility = :visibility, reviewer_id = :reviewerId, approved_at = :approvedAt, description = :description " +
|
||||
@SqlUpdate("UPDATE project_versions SET visibility = :visibility, reviewer_id = :reviewerId, approved_at = :approvedAt, description = :description, " +
|
||||
"review_state = :reviewState " +
|
||||
"WHERE id = :id")
|
||||
void update(@BindBean ProjectVersionsTable projectVersionsTable);
|
||||
|
||||
|
@ -0,0 +1,48 @@
|
||||
package me.minidigger.hangar.db.dao;
|
||||
|
||||
import me.minidigger.hangar.db.model.ProjectVersionReviewsTable;
|
||||
import org.jdbi.v3.sqlobject.config.KeyColumn;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindBean;
|
||||
import org.jdbi.v3.sqlobject.customizer.Timestamped;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
@Repository
|
||||
@RegisterBeanMapper(ProjectVersionReviewsTable.class)
|
||||
public interface ProjectVersionReviewDao {
|
||||
|
||||
@Timestamped
|
||||
@GetGeneratedKeys
|
||||
@SqlUpdate("INSERT INTO project_version_reviews (created_at, version_id, user_id, comment) VALUES (:now, :versionId, :userId, :comment)")
|
||||
ProjectVersionReviewsTable insert(@BindBean ProjectVersionReviewsTable projectVersionReviewsTable);
|
||||
|
||||
@SqlUpdate("UPDATE project_version_reviews SET ended_at = :endedAt, created_at = :createdAt, comment = :comment WHERE id = :id")
|
||||
void update(@BindBean ProjectVersionReviewsTable projectVersionReviewsTable);
|
||||
|
||||
@KeyColumn("userName")
|
||||
@SqlQuery("SELECT pvr.*, u.name userName FROM project_version_reviews pvr JOIN users u ON pvr.user_id = u.id WHERE pvr.id = :id")
|
||||
Entry<String, ProjectVersionReviewsTable> getById(long id);
|
||||
|
||||
@KeyColumn("userName")
|
||||
@SqlQuery("SELECT pvr.*, u.name userName " +
|
||||
"FROM project_version_reviews pvr" +
|
||||
" JOIN users u ON pvr.user_id = u.id " +
|
||||
"WHERE pvr.version_id = :versionId ORDER BY pvr.created_at DESC")
|
||||
List<Entry<String, ProjectVersionReviewsTable>> getMostRecentReviews(long versionId);
|
||||
|
||||
@KeyColumn("userName")
|
||||
@SqlQuery("SELECT pvr.*, u.name userName " +
|
||||
"FROM project_version_reviews pvr" +
|
||||
" JOIN users u ON pvr.user_id = u.id " +
|
||||
"WHERE " +
|
||||
" pvr.version_id = :versionId AND " +
|
||||
" pvr.ended_at IS NULL " +
|
||||
"ORDER BY pvr.created_at")
|
||||
List<Entry<String, ProjectVersionReviewsTable>> getUnfinishedReviews(long versionId);
|
||||
}
|
@ -14,6 +14,22 @@ public class ProjectVersionReviewsTable {
|
||||
private OffsetDateTime endedAt;
|
||||
private JSONB comment;
|
||||
|
||||
public ProjectVersionReviewsTable(ProjectVersionReviewsTable projectVersionReviewsTable) {
|
||||
this.id = projectVersionReviewsTable.id;
|
||||
this.versionId = projectVersionReviewsTable.versionId;
|
||||
this.userId = projectVersionReviewsTable.userId;
|
||||
this.createdAt = projectVersionReviewsTable.createdAt;
|
||||
this.endedAt = projectVersionReviewsTable.endedAt;
|
||||
this.comment = projectVersionReviewsTable.comment;
|
||||
}
|
||||
|
||||
public ProjectVersionReviewsTable(long versionId, long userId, JSONB comment) {
|
||||
this.versionId = versionId;
|
||||
this.userId = userId;
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public ProjectVersionReviewsTable() { }
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
|
@ -6,10 +6,10 @@ import java.time.OffsetDateTime;
|
||||
|
||||
public class Review {
|
||||
|
||||
private Version version;
|
||||
private long userId;
|
||||
private OffsetDateTime endedAt;
|
||||
private String message;
|
||||
private final Version version;
|
||||
private final long userId;
|
||||
private final OffsetDateTime endedAt;
|
||||
private final String message;
|
||||
|
||||
public Review(Version version, long userId, OffsetDateTime endedAt, String message) {
|
||||
this.version = version;
|
||||
|
@ -6,8 +6,8 @@ import java.time.OffsetDateTime;
|
||||
|
||||
public class ReviewActivity extends Activity {
|
||||
|
||||
private OffsetDateTime endedAt;
|
||||
private Review id;
|
||||
private final OffsetDateTime endedAt;
|
||||
private final Review id;
|
||||
|
||||
public ReviewActivity(OffsetDateTime endedAt, Review id, ProjectNamespace project) {
|
||||
super(project);
|
||||
|
@ -0,0 +1,47 @@
|
||||
package me.minidigger.hangar.model.viewhelpers;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import me.minidigger.hangar.db.customtypes.JSONB;
|
||||
import me.minidigger.hangar.db.model.ProjectVersionReviewsTable;
|
||||
import me.minidigger.hangar.service.ReviewService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class VersionReview extends ProjectVersionReviewsTable {
|
||||
|
||||
private final String userName;
|
||||
private final List<VersionReviewMessage> messages = new ArrayList<>();
|
||||
|
||||
public VersionReview(String userName, ProjectVersionReviewsTable review) throws JsonProcessingException {
|
||||
super(review);
|
||||
this.userName = userName;
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
if (this.getComment().getJson().hasNonNull("messages")) {
|
||||
for (JsonNode node : this.getComment().getJson().get("messages")) {
|
||||
this.messages.add(mapper.treeToValue(node, VersionReviewMessage.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public List<VersionReviewMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void addMessage(VersionReviewMessage message, ReviewService reviewService) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectNode comment = new ObjectNode(JsonNodeFactory.instance);
|
||||
messages.add(message);
|
||||
comment.set("messages", mapper.valueToTree(messages));
|
||||
this.setComment(new JSONB(comment));
|
||||
reviewService.update(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package me.minidigger.hangar.model.viewhelpers;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
|
||||
public class VersionReviewMessage {
|
||||
|
||||
private String message;
|
||||
private long time;
|
||||
private String action;
|
||||
|
||||
public VersionReviewMessage(String message) {
|
||||
this.message = message;
|
||||
this.time = System.currentTimeMillis();
|
||||
this.action = "message";
|
||||
}
|
||||
|
||||
public VersionReviewMessage(String message, long time, String action) {
|
||||
this.message = message;
|
||||
this.time = time;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public VersionReviewMessage() { }
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void setTime(long time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public void setAction(String action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getFormattedTime() {
|
||||
return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(OffsetDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isTakeover() {
|
||||
return action.equalsIgnoreCase("takeover");
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isStop() {
|
||||
return action.equalsIgnoreCase("stop");
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package me.minidigger.hangar.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import me.minidigger.hangar.db.dao.HangarDao;
|
||||
import me.minidigger.hangar.db.dao.ProjectVersionReviewDao;
|
||||
import me.minidigger.hangar.db.model.ProjectVersionReviewsTable;
|
||||
import me.minidigger.hangar.model.viewhelpers.VersionReview;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ReviewService {
|
||||
|
||||
private final HangarDao<ProjectVersionReviewDao> projectVersionReviewDao;
|
||||
|
||||
@Autowired
|
||||
public ReviewService(HangarDao<ProjectVersionReviewDao> projectVersionReviewDao) {
|
||||
this.projectVersionReviewDao = projectVersionReviewDao;
|
||||
}
|
||||
|
||||
public ProjectVersionReviewsTable insert(ProjectVersionReviewsTable projectVersionReviewsTable) {
|
||||
return projectVersionReviewDao.get().insert(projectVersionReviewsTable);
|
||||
}
|
||||
|
||||
public void update(ProjectVersionReviewsTable projectVersionReviewsTable) {
|
||||
projectVersionReviewDao.get().update(projectVersionReviewsTable);
|
||||
}
|
||||
|
||||
public VersionReview getById(long reviewId) {
|
||||
Entry<String, ProjectVersionReviewsTable> entry = projectVersionReviewDao.get().getById(reviewId);
|
||||
if (entry == null) return null;
|
||||
try {
|
||||
return new VersionReview(entry.getKey(), entry.getValue());
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<VersionReview> getRecentReviews(long versionId) {
|
||||
return projectVersionReviewDao.get().getMostRecentReviews(versionId).stream().map(entry -> {
|
||||
try {
|
||||
return new VersionReview(entry.getKey(), entry.getValue());
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<VersionReview> getUnfinishedReviews(long versionId) {
|
||||
return projectVersionReviewDao.get().getUnfinishedReviews(versionId).stream().map(entry -> {
|
||||
try {
|
||||
return new VersionReview(entry.getKey(), entry.getValue());
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public VersionReview getMostRecentUnfinishedReview(long versionId) {
|
||||
return getUnfinishedReviews(versionId).stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
@ -201,7 +201,7 @@ public class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
public List<ReviewActivity> getRewiewActivity(String username) {
|
||||
public List<ReviewActivity> getReviewActivity(String username) {
|
||||
return userDao.get().getReviewActivity(username);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,9 @@ import me.minidigger.hangar.config.hangar.HangarConfig;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
|
||||
public class TemplateHelper {
|
||||
|
||||
@ -34,4 +37,8 @@ public class TemplateHelper {
|
||||
return String.format("%.1f %cB", size.doubleValue() / (1L << (z * 10)), " KMGTPE".charAt((int) z));
|
||||
}
|
||||
}
|
||||
|
||||
public String prettifyDate(OffsetDateTime dateTime) {
|
||||
return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(dateTime);
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@
|
||||
<a href="${routes.getRouteUrl("users.showProjects", v.p.project.ownerName)}">
|
||||
<strong>${v.p.project.ownerName}</strong>
|
||||
</a>
|
||||
released this version on ${(v.v.createdAt).format("YYYY-mm-dd HH:mm:ss")}
|
||||
released this version on ${utils.prettifyDate(v.v.createdAt)}
|
||||
</p>
|
||||
|
||||
<!-- Buttons -->
|
||||
@ -55,7 +55,7 @@
|
||||
<#if headerData.globalPerm(Permission.Reviewer)>
|
||||
<#if v.approvedBy?has_content>
|
||||
<i class="minor">
|
||||
<#assign msgArgs=[v.approvedBy, (v.v.approvedAt).format("YYYY-mm-dd HH:mm:ss")] />
|
||||
<#assign msgArgs=[v.approvedBy, utils.prettifyDate(v.v.approvedAt)] />
|
||||
<@spring.messageArgs "version.approved.info" msgArgs />
|
||||
</i>
|
||||
</#if>
|
||||
|
@ -18,33 +18,35 @@
|
||||
-->
|
||||
|
||||
<#assign scripts>
|
||||
<script @CSPNonce.attr type="text/javascript" src="<@hangar.url "javascripts/review.js" />"></script>
|
||||
<script @CSPNonce.attr>versionPath = '${helper.urlEncode(project.ownerName)}/${helper.urlEncode(project.slug)}/versions/${helper.urlEncode(version.name)}'</script>
|
||||
<script <#--@CSPNonce.attr--> type="text/javascript" src="<@hangar.url "javascripts/review.js" />"></script>
|
||||
<script <#--@CSPNonce.attr-->>versionPath = '${utils.urlEncode(project.project.ownerName)}/${utils.urlEncode(project.project.slug)}/versions/${utils.urlEncode(version.v.versionString)}'</script>
|
||||
</#assign>
|
||||
|
||||
<#assign message><@spring.messageArgs code="review.title" args=[project.name, version.name] /></#assign>
|
||||
<#assign ReviewState=@helper["me.minidigger.hangar.model.generated.ReviewState"] />
|
||||
<#-- @ftlvariable name="ReviewState" type="me.minidigger.hangar.model.generated.ReviewState" -->
|
||||
<#assign message><@spring.messageArgs code="review.title" args=[project.project.name, version.v.versionString] /></#assign>
|
||||
<@base.base title=message additionalScripts=scripts>
|
||||
<div class="row">
|
||||
<div class="col-md-12 header-flags">
|
||||
<div class="clearfix">
|
||||
<h1 class="pull-left">${project.name} <i>${version.versionString}</i></h1>
|
||||
<h1 class="pull-left">${project.project.name} <i>${version.v.versionString}</i></h1>
|
||||
</div>
|
||||
<p class="user date pull-left">
|
||||
<a href="${routes.getRouteUrl("users.showProjects", project.ownerName)}">
|
||||
<strong>@project.ownerName</strong>
|
||||
<a href="${routes.getRouteUrl("users.showProjects", project.project.ownerName)}">
|
||||
<strong>${project.project.ownerName}</strong>
|
||||
</a>
|
||||
released this version on ${version.createdAt?string.long}
|
||||
released this version on ${utils.prettifyDate(version.v.createdAt)}
|
||||
</p>
|
||||
<#if !version.reviewState.isChecked>
|
||||
<#if !version.v.reviewState.checked>
|
||||
<div class="pull-right">
|
||||
<span class="btn-group-sm">
|
||||
<a href="#" class="btn btn-info btn-skip-review"><#if version.reviewState != ReviewState.Backlog> Remove from queue <#else> Add to queue </#if></a>
|
||||
<a href="${routes.getRouteUrl("projects.show", project.ownerName, project.slug)}" class="btn btn-info">Project Page</a>
|
||||
<a href="${routes.getRouteUrl("versions.downloadJar", project.ownerName, project.slug, version.versionString, "")}" class="btn btn-info">Download File</a>
|
||||
<a href="#" class="btn btn-info btn-skip-review"><#if version.v.reviewState != ReviewState.BACKLOG> Remove from queue <#else> Add to queue </#if></a>
|
||||
<a href="${routes.getRouteUrl("projects.show", project.project.ownerName, project.project.slug)}" class="btn btn-info">Project Page</a>
|
||||
<a href="${routes.getRouteUrl("versions.downloadJar", project.project.ownerName, project.project.slug, version.v.versionString, "")}" class="btn btn-info">Download File</a>
|
||||
</span>
|
||||
<span class="btn-group-sm">
|
||||
<#if mostRecentUnfinishedReview??>
|
||||
<#if request.headerData.isCurrentUser(mostRecentUnfinishedReview.userId)>
|
||||
<#if headerData.isCurrentUser(mostRecentUnfinishedReview.userId)>
|
||||
<button class="btn btn-review-stop btn-danger"><i class="fas fa-stop-circle"></i> <@spring.message "review.stop" /></button>
|
||||
<button class="btn btn-review-approve btn-success"><i class="fas fa-thumbs-up"></i> <@spring.message "user.queue.approve" /></button>
|
||||
<button class="btn btn-review-approve-partial btn-success"><i class="fas fa-thumbs-up"></i> <@spring.message "user.queue.approvePartial" /></button>
|
||||
@ -66,12 +68,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<#if mostRecentUnfinishedReview??>
|
||||
<#if request.headerData.isCurrentUser(mostRecentUnfinishedReview.userId)>
|
||||
<#if headerData.isCurrentUser(mostRecentUnfinishedReview.userId)>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="input-group" style="width: 100%;
|
||||
margin-top: 1em;">
|
||||
<textarea type="text" class="form-control textarea-addmessage" placeholder="Message"></textarea>
|
||||
<textarea type="text" class="form-control textarea-addmessage" placeholder="Message" autocomplete="off"></textarea>
|
||||
<div class="input-group-addon btn btn-review-addmessage-submit btn-primary"><i class="fas fa-clipboard"></i> <@spring.message "review.addmessage" /></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -82,7 +84,7 @@
|
||||
<div class="col-md-12">
|
||||
<h2><@spring.message "review.log" /></h2>
|
||||
</div>
|
||||
<#if reviews.isEmpty>
|
||||
<#if !reviews?has_content>
|
||||
<div class="col-md-12">
|
||||
<div class="alert-review alert alert-info" role="alert">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
@ -94,70 +96,70 @@
|
||||
<div class="col-md-12">
|
||||
<table class="table table-condensed setting-no-border table-review-log">
|
||||
<tbody>
|
||||
@reviews.reverse.zipWithIndex.map { case (t, index) =>
|
||||
@defining((t._1, t._2)) { case (item, name) =>
|
||||
<#if item.endedAt??>
|
||||
<#if reviews.size > (reviews.size - index)>
|
||||
<#if (item.endedAt?string.long).equalsIgnoreCase(reviews.reverse(reviews.size - index - 1)._1.createdAt?string.long)>
|
||||
<tr>
|
||||
<td>${item.endedAt?string.long}</td>
|
||||
<td>
|
||||
<strong>${name!"Unknown"}</strong>
|
||||
took over from
|
||||
<strong>${name!"Unknown"}</strong>
|
||||
<#if !item.decodeMessages.exists(_.isTakeover)>
|
||||
<i>- no message provided -</i>
|
||||
<#else>
|
||||
<i>${item.decodeMessages.filter(_.isTakeover).head.render}</i>
|
||||
</#if>
|
||||
</td>
|
||||
</tr>
|
||||
<#else>
|
||||
<tr>
|
||||
<td>${item.endedAt?string.long}</td>
|
||||
<td><strong>${name!"Unknown"}</strong> stopped</td>
|
||||
</tr>
|
||||
</#if>
|
||||
<#-- @ftlvariable name="review" type="me.minidigger.hangar.model.viewhelpers.VersionReview" -->
|
||||
<#list reviews as review>
|
||||
<#if review.endedAt??>
|
||||
<#if reviews?size gt (reviews?size - review?index)>
|
||||
<#if review.endedAt.toEpochSecond() == reviews?reverse[reviews?size - review?index - 1].endedAt.toEpochSecond()>
|
||||
<tr>
|
||||
<td>${utils.prettifyDate(review.endedAt)}</td>
|
||||
<td>
|
||||
<strong>${review.userName!"Unknown"}</strong>
|
||||
took over from
|
||||
<strong>${review.userName!"Unknown"}</strong>
|
||||
<#if !review.messages?filter(m -> m.takeover)?has_content>
|
||||
<i>- no message provided -</i>
|
||||
<#else>
|
||||
<i>${markdownService.render(review.messages?filter(m -> m.takeover)?first.message)}</i>
|
||||
</#if>
|
||||
</td>
|
||||
</tr>
|
||||
<#else>
|
||||
<#if version.approvedAt??>
|
||||
<tr>
|
||||
<td>${item.endedAt?string.long}</td>
|
||||
<td><strong>${name!"Unknown"}</strong> approved</td>
|
||||
</tr>
|
||||
<#else>
|
||||
<tr>
|
||||
<td>${item.endedAt?string.long}</td>
|
||||
<td>
|
||||
<strong>${name!"Unknown"}</strong>
|
||||
stopped
|
||||
<br>
|
||||
<#if !item.decodeMessages.exists(_.isStop)>
|
||||
<i>- no message provided -</i>
|
||||
<#else>
|
||||
<i>${item.decodeMessages.filter(_.isStop).head.render}</i>
|
||||
</#if>
|
||||
</td>
|
||||
</tr>
|
||||
</#if>
|
||||
<tr>
|
||||
<td>${utils.prettifyDate(review.endedAt)}</td>
|
||||
<td><strong>${review.userName!"Unknown"}</strong> stopped</td>
|
||||
</tr>
|
||||
</#if>
|
||||
<#else>
|
||||
<#if version.approvedAt??>
|
||||
<tr>
|
||||
<td>${utils.prettifyDate(review.endedAt)}</td>
|
||||
<td><strong>${review.userName!"Unknown"}</strong> approved</td>
|
||||
</tr>
|
||||
<#else>
|
||||
<tr>
|
||||
<td>${utils.prettifyDate(review.endedAt)}</td>
|
||||
<td>
|
||||
<strong>${review.userName!"Unknown"}</strong>
|
||||
stopped
|
||||
<br>
|
||||
<#if !review.messages?filter(m -> m.stop)?has_content>
|
||||
<i>- no message provided -</i>
|
||||
<#else>
|
||||
<i>${markdownService.render(review.messages?filter(m -> m.stop)?first.message)}</i>
|
||||
</#if>
|
||||
</td>
|
||||
</tr>
|
||||
</#if>
|
||||
</#if>
|
||||
@item.decodeMessages.filterNot(_.isTakeover).filterNot(_.isStop).reverse.map { message =>
|
||||
<tr>
|
||||
<td>${message.getTime(messages.lang.locale)}</td>
|
||||
<td>
|
||||
<strong>${name!"Unknown"}</strong>
|
||||
added message
|
||||
<br>
|
||||
<i>${message.render}</i>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</#if>
|
||||
<#list review.getMessages()?filter(m -> !m.takeover && !m.stop)?reverse as message>
|
||||
<#-- @ftlvariable name="message" type="me.minidigger.hangar.model.viewhelpers.VersionReviewMessage" -->
|
||||
<tr>
|
||||
<td>${item.createdAt.value?string.long}</td>
|
||||
<td><strong>${name!"Unknown"}</strong> started a review</td>
|
||||
<td>${message.getFormattedTime()}</td> <#-- TODO per locale-->
|
||||
<td>
|
||||
<strong>${review.userName!"Unknown"}</strong>
|
||||
added message
|
||||
<br>
|
||||
<i>${markdownService.render(message.message)}</i>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</#list>
|
||||
<tr>
|
||||
<td>${utils.prettifyDate(review.createdAt)}</td>
|
||||
<td><strong>${review.userName!"Unknown"}</strong> started a review</td>
|
||||
</tr>
|
||||
</#list>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user