version reviews

This commit is contained in:
Jake Potrebic 2020-08-06 16:35:23 -07:00
parent 6a528b834f
commit d8e94d807b
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
17 changed files with 567 additions and 119 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -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")

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -201,7 +201,7 @@ public class UserService {
}
}
public List<ReviewActivity> getRewiewActivity(String username) {
public List<ReviewActivity> getReviewActivity(String username) {
return userDao.get().getReviewActivity(username);
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>