Skip to content

Commit 108acf7

Browse files
committed
feat: admin can add a comment to a series.
Implementation details: a comment can be added only if there is no comment yet. This is a protection against possible race or other unforeseeable circumstances that might lead to data loss. A full implementation of the editing can be added later so this can be considered as the first iteration. Fix #785
1 parent 4e2ef06 commit 108acf7

16 files changed

+248
-5
lines changed

NEWS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- (feature) a series can be marked as a similar to another one
33
- (feature) series images can be replaced
44
- (feature) add preliminary support for hidden images
5+
- (feature) admin can add a comment to a series
56

67
0.4.3
78
- (feature) add support for Ukrainian hryvnia

src/main/frontend/src/components/AddCommentForm.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ class AddCommentForm extends React.PureComponent {
5454

5555
if (data.hasOwnProperty('fieldErrors')) {
5656
const fieldErrors = [];
57-
if (data.fieldErrors.comment) {
58-
fieldErrors.push(...data.fieldErrors.comment);
57+
if (data.fieldErrors.value) {
58+
fieldErrors.push(...data.fieldErrors.value);
5959
}
6060
this.setState({
6161
isDisabled: false,

src/main/java/ru/mystamps/web/feature/series/JdbcSeriesDao.java

+19
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public class JdbcSeriesDao implements SeriesDao {
4848
@Value("${series.create}")
4949
private String createSeriesSql;
5050

51+
@Value("${series.add_comment}")
52+
private String addCommentSql;
53+
5154
@Value("${series.mark_as_modified}")
5255
private String markAsModifiedSql;
5356

@@ -136,6 +139,22 @@ public Integer add(AddSeriesDbDto series) {
136139
return Integer.valueOf(holder.getKey().intValue());
137140
}
138141

142+
@Override
143+
public void addComment(Integer seriesId, String comment) {
144+
Map<String, Object> params = new HashMap<>();
145+
params.put("series_id", seriesId);
146+
params.put("comment", comment);
147+
148+
int affected = jdbcTemplate.update(addCommentSql, params);
149+
150+
// @todo #785 Update series: handle refuse to update an existing comment gracefully
151+
Validate.validState(
152+
affected == 1,
153+
"Unexpected number of affected rows after updating series: %d",
154+
affected
155+
);
156+
}
157+
139158
/**
140159
* @author Sergey Chechenev
141160
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (C) 2009-2020 Slava Semushin <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.feature.series;
19+
20+
import lombok.RequiredArgsConstructor;
21+
import org.springframework.http.ResponseEntity;
22+
import org.springframework.validation.annotation.Validated;
23+
import org.springframework.web.bind.annotation.PatchMapping;
24+
import org.springframework.web.bind.annotation.PathVariable;
25+
import org.springframework.web.bind.annotation.RequestBody;
26+
import org.springframework.web.bind.annotation.RestController;
27+
import ru.mystamps.web.support.spring.mvc.PatchRequest;
28+
import ru.mystamps.web.support.spring.mvc.PatchRequest.Operation;
29+
30+
import javax.servlet.http.HttpServletResponse;
31+
import javax.validation.Valid;
32+
import javax.validation.constraints.NotEmpty;
33+
import java.io.IOException;
34+
import java.util.List;
35+
36+
@Validated
37+
@RestController
38+
@RequiredArgsConstructor
39+
class RestSeriesController {
40+
41+
private final SeriesService seriesService;
42+
43+
// @todo #785 Update series: add integration test
44+
// @todo #785 Update series: add validation for a comment
45+
@PatchMapping(SeriesUrl.INFO_SERIES_PAGE)
46+
@SuppressWarnings("PMD.TooFewBranchesForASwitchStatement")
47+
public ResponseEntity<Void> updateSeries(
48+
@PathVariable("id") Integer seriesId,
49+
@RequestBody @Valid @NotEmpty List<@Valid PatchRequest> patches,
50+
HttpServletResponse response) throws IOException {
51+
52+
if (seriesId == null) {
53+
response.sendError(HttpServletResponse.SC_NOT_FOUND);
54+
return null;
55+
}
56+
57+
if (!seriesService.isSeriesExist(seriesId)) {
58+
response.sendError(HttpServletResponse.SC_NOT_FOUND);
59+
return null;
60+
}
61+
62+
for (PatchRequest patch : patches) {
63+
if (patch.op != Operation.add) {
64+
// @todo #785 Update series: properly fail on non-supported operations
65+
continue;
66+
}
67+
68+
switch (patch.path) {
69+
case "/comment":
70+
seriesService.addComment(seriesId, patch.value);
71+
break;
72+
default:
73+
// @todo #785 Update series: properly fail on invalid path
74+
break;
75+
}
76+
}
77+
78+
return ResponseEntity.noContent().build();
79+
}
80+
81+
}
82+

src/main/java/ru/mystamps/web/feature/series/SeriesConfig.java

+5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public SeriesController seriesController() {
6969
);
7070
}
7171

72+
@Bean
73+
public RestSeriesController restSeriesController() {
74+
return new RestSeriesController(seriesService);
75+
}
76+
7277
}
7378

7479
@Import(Daos.class)

src/main/java/ru/mystamps/web/feature/series/SeriesDao.java

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
@SuppressWarnings("PMD.TooManyMethods")
2525
public interface SeriesDao {
2626
Integer add(AddSeriesDbDto series);
27+
void addComment(Integer seriesId, String comment);
2728
void markAsModified(Integer seriesId, Date updateAt, Integer updatedBy);
2829
List<SitemapInfoDto> findAllForSitemap();
2930
List<SeriesLinkDto> findSimilarSeries(Integer seriesId, String lang);

src/main/java/ru/mystamps/web/feature/series/SeriesService.java

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
@SuppressWarnings("PMD.TooManyMethods")
2525
public interface SeriesService {
2626
Integer add(AddSeriesDto dto, Integer userId, boolean userCanAddComments);
27+
void addComment(Integer seriesId, String comment);
2728
void addImageToSeries(AddImageDto dto, Integer seriesId, Integer userId);
2829
void replaceImage(ReplaceImageDto dto, Integer seriesId, Integer userId);
2930
long countAll();

src/main/java/ru/mystamps/web/feature/series/SeriesServiceImpl.java

+17
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ public Integer add(AddSeriesDto dto, Integer userId, boolean userCanAddComments)
118118
return id;
119119
}
120120

121+
// @todo #785 SeriesServiceImpl.addComment(): add unit tests
122+
@Override
123+
@Transactional
124+
@PreAuthorize(HasAuthority.ADD_COMMENTS_TO_SERIES)
125+
public void addComment(Integer seriesId, String comment) {
126+
Validate.isTrue(seriesId != null, "Series id must be non null");
127+
128+
// We don't touch updated_at/updated_by fields because:
129+
// - a comment is a meta information that is visible only by admins.
130+
// From user's point of view, this field doesn't exist
131+
// - updated_at field is used by logic for sitemap.xml generation
132+
// and we don't want to affect this
133+
seriesDao.addComment(seriesId, comment);
134+
135+
log.info("Series #{}: a comment has been added", seriesId);
136+
}
137+
121138
@Override
122139
@Transactional
123140
@PreAuthorize("isAuthenticated()")

src/main/java/ru/mystamps/web/feature/site/ResourceUrl.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public final class ResourceUrl {
3232
public static final String STATIC_RESOURCES_URL = "https://stamps.filezz.ru";
3333

3434
// MUST be updated when any of our resources were modified
35-
public static final String RESOURCES_VERSION = "v0.4.3.8";
35+
public static final String RESOURCES_VERSION = "v0.4.3.9";
3636

3737
// CheckStyle: ignore LineLength for next 15 lines
3838
private static final String CATALOG_UTILS_JS = "/public/js/" + RESOURCES_VERSION + "/CatalogUtils.min.js";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright (C) 2009-2020 Slava Semushin <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
19+
package ru.mystamps.web.support.spring.mvc;
20+
21+
import lombok.Getter;
22+
import lombok.Setter;
23+
import lombok.ToString;
24+
25+
import javax.validation.constraints.NotEmpty;
26+
import javax.validation.constraints.NotNull;
27+
28+
// See for details: http://jsonpatch.com
29+
@Getter
30+
@Setter
31+
@ToString
32+
public class PatchRequest {
33+
34+
public enum Operation {
35+
add, copy, move, remove, replace, test;
36+
}
37+
38+
// @todo #785 Update series: add integration test for required "op" field
39+
@NotNull
40+
private Operation op;
41+
42+
// @todo #785 Update series: add integration test for non-empty "path" field
43+
@NotEmpty
44+
private String path;
45+
46+
// @todo #785 Update series: add integration test for non-empty "value" field
47+
@NotEmpty
48+
private String value;
49+
50+
}

src/main/java/ru/mystamps/web/support/spring/mvc/RestExceptionHandler.java

+18
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.springframework.web.bind.annotation.ExceptionHandler;
2626
import org.springframework.web.bind.annotation.RestControllerAdvice;
2727

28+
import javax.validation.ConstraintViolationException;
29+
2830
@RestControllerAdvice
2931
public class RestExceptionHandler {
3032

@@ -45,4 +47,20 @@ public ResponseEntity<ValidationErrors> handleMethodArgumentNotValidException(
4547
);
4648
}
4749

50+
// handle cases like "@RequestBody @Valid @NotEmpty List<@Valid PatchRequest> patches"
51+
@ExceptionHandler(ConstraintViolationException.class)
52+
public ResponseEntity<ValidationErrors> handleConstraintViolationException(
53+
ConstraintViolationException ex) {
54+
55+
if (ex == null) {
56+
LOG.warn("Couldn't handle ConstraintViolationException that is null");
57+
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
58+
}
59+
60+
return new ResponseEntity<>(
61+
new ValidationErrors(ex.getConstraintViolations()),
62+
HttpStatus.BAD_REQUEST
63+
);
64+
}
65+
4866
}

src/main/java/ru/mystamps/web/support/spring/mvc/ValidationErrors.java

+37
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@
2121
import org.springframework.validation.Errors;
2222
import org.springframework.validation.FieldError;
2323

24+
import javax.validation.ConstraintViolation;
25+
import javax.validation.ElementKind;
26+
import javax.validation.Path.Node;
2427
import java.util.ArrayList;
2528
import java.util.HashMap;
2629
import java.util.List;
2730
import java.util.Map;
31+
import java.util.Set;
2832

2933
@Getter
3034
public class ValidationErrors {
@@ -36,15 +40,48 @@ public ValidationErrors(Errors errors) {
3640
.forEach(this::extractFieldError);
3741
}
3842

43+
public ValidationErrors(Set<ConstraintViolation<?>> violations) {
44+
violations.forEach(this::extractFieldError);
45+
}
46+
3947
private void extractFieldError(FieldError error) {
4048
String name = error.getField();
4149
String message = error.getDefaultMessage();
4250

4351
getErrorsByFieldName(name).add(message);
4452
}
4553

54+
// @todo #785 Improve error handling for requests with a list of objects
55+
private void extractFieldError(ConstraintViolation<?> violation) {
56+
// Extract a field name from a path: updateSeries.patches[0].value -> value
57+
// In this case, we loose index (see Node.getIndex() and Node.isInIterable()) but
58+
// as now we always have requests with a single object, that's not severe.
59+
// Ideally, for multiple objects we should return ValidationErrors[] instead of
60+
// ValidationErrors
61+
Node last = getLastOrNull(violation.getPropertyPath());
62+
String name;
63+
if (last == null || last.getKind() != ElementKind.PROPERTY) {
64+
// fallback to a field path for unsupported kinds
65+
name = violation.getPropertyPath().toString();
66+
} else {
67+
name = last.getName();
68+
}
69+
70+
String message = violation.getMessage();
71+
72+
getErrorsByFieldName(name).add(message);
73+
}
74+
4675
private List<String> getErrorsByFieldName(String name) {
4776
return fieldErrors.computeIfAbsent(name, key -> new ArrayList<>());
4877
}
4978

79+
private static <T> T getLastOrNull(Iterable<T> elements) {
80+
T last = null;
81+
for (T elem : elements) {
82+
last = elem;
83+
}
84+
return last;
85+
}
86+
5087
}

src/main/java/ru/mystamps/web/support/spring/security/HasAuthority.java

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
@SuppressWarnings({ "checkstyle:linelength", "PMD.AvoidDuplicateLiterals" })
2121
public final class HasAuthority {
2222
// Constants sorted in an ascending order.
23+
public static final String ADD_COMMENTS_TO_SERIES = "hasAuthority('" + StringAuthority.ADD_COMMENTS_TO_SERIES + "')";
2324
public static final String ADD_PARTICIPANT = "hasAuthority('" + StringAuthority.ADD_PARTICIPANT + "')";
2425
@SuppressWarnings("PMD.LongVariable")
2526
public static final String ADD_SERIES_PRICE_AND_COLLECTION_OWNER_OR_VIEW_ANY_ESTIMATION =

src/main/java/ru/mystamps/web/support/spring/security/SecurityConfig.java

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ protected void configure(HttpSecurity http) throws Exception {
109109
.mvcMatchers(CountryUrl.ADD_COUNTRY_PAGE).hasAuthority(StringAuthority.CREATE_COUNTRY)
110110
.mvcMatchers(ParticipantUrl.ADD_PARTICIPANT_PAGE).hasAuthority(StringAuthority.ADD_PARTICIPANT)
111111
.mvcMatchers(SeriesUrl.ADD_SERIES_PAGE).hasAuthority(StringAuthority.CREATE_SERIES)
112+
.mvcMatchers(HttpMethod.PATCH, SeriesUrl.INFO_SERIES_PAGE).hasAuthority(StringAuthority.ADD_COMMENTS_TO_SERIES)
112113
.mvcMatchers(SeriesImportUrl.REQUEST_IMPORT_SERIES_PAGE).hasAuthority(StringAuthority.IMPORT_SERIES)
113114
.mvcMatchers(SiteUrl.SITE_EVENTS_PAGE).hasAuthority(StringAuthority.VIEW_SITE_EVENTS)
114115
.mvcMatchers(CategoryUrl.SUGGEST_SERIES_CATEGORY).hasAuthority(StringAuthority.CREATE_SERIES)

src/main/resources/sql/series_dao_queries.properties

+6
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ VALUES \
4141
, :updated_by \
4242
)
4343

44+
series.add_comment = \
45+
UPDATE series \
46+
SET comment = :comment \
47+
WHERE id = :series_id \
48+
AND comment IS NULL
49+
4450
series.mark_as_modified = \
4551
UPDATE series \
4652
SET updated_at = :updated_at \

src/main/webapp/WEB-INF/views/series/info.html

+6-2
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ <h5 th:text="#{t_hidden_images}">Hidden images</h5>
398398

399399
<div id="add-catalog-numbers" sec:authorize="hasAuthority('CREATE_SERIES')"></div>
400400

401-
<div id="add-comment" sec:authorize="hasAuthority('ADD_COMMENTS_TO_SERIES')"></div>
401+
<div id="add-comment" sec:authorize="hasAuthority('ADD_COMMENTS_TO_SERIES')" th:if="${series.comment == null}"></div>
402402

403403
<div id="similar-series" class="row" th:if="${not #lists.isEmpty(similarSeries)}">
404404
<div class="col-sm-10 col-sm-offset-2">
@@ -1058,15 +1058,19 @@ <h5 th:text="#{t_add_info_who_selling_series}">Add info about selling/buying thi
10581058
/*/-->
10591059

10601060
<!--/*/
1061-
<th:block sec:authorize="hasAuthority('ADD_COMMENTS_TO_SERIES')">
1061+
<th:block sec:authorize="hasAuthority('ADD_COMMENTS_TO_SERIES')" th:if="${series.comment == null}">
10621062
/*/-->
10631063
<script src="../../../../../../target/classes/js/components/AddCommentForm.js" th:src="${ADD_COMMENT_FORM_JS}"></script>
10641064
<script th:inline="javascript">
10651065
/*[+
10661066
var addCommentProps = {
1067+
'url': [[ '__@{${INFO_SERIES_PAGE}(id=${series.id})}__' ]],
10671068
'csrfHeaderName': [[ ${_csrf.headerName} ]],
10681069
'csrfTokenValue': [[ ${_csrf.token} ]],
10691070
'l10n': {
1071+
't_server_error': [[ #{t_server_error} ]],
1072+
't_comment': [[ #{t_comment} ]],
1073+
't_add': [[ #{t_add} ]]
10701074
}
10711075
};
10721076
+]*/

0 commit comments

Comments
 (0)