Skip to content

Commit 1391152

Browse files
committed
feat: admin can mark a series as a similar to another series.
Fix #1280
1 parent f3b4ad5 commit 1391152

20 files changed

+340
-13
lines changed

NEWS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
0.x (upcoming release)
2+
- (feature) a series can be marked as a similar to another one
23

34
0.4.3
45
- (feature) add support for Ukrainian hryvnia
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//
2+
// IMPORTANT:
3+
// You must update ResourceUrl.RESOURCES_VERSION each time whenever you're modified this file!
4+
//
5+
6+
// @todo #1280 SimilarSeriesForm: add tests
7+
class SimilarSeriesForm extends React.Component {
8+
9+
constructor(props) {
10+
super(props);
11+
this.state = {
12+
seriesId: props.seriesId,
13+
similarSeriesId: '',
14+
isDisabled: false,
15+
hasServerError: false,
16+
validationErrors: []
17+
};
18+
this.handleSubmit = this.handleSubmit.bind(this);
19+
this.handleChange = this.handleChange.bind(this);
20+
}
21+
22+
handleChange(event) {
23+
event.preventDefault();
24+
this.setState({
25+
similarSeriesId: event.target.value
26+
});
27+
}
28+
29+
handleSubmit(event) {
30+
event.preventDefault();
31+
32+
this.setState({
33+
isDisabled: true,
34+
hasServerError: false,
35+
validationErrors: []
36+
});
37+
38+
const similarSeriesId = parseInt(this.state.similarSeriesId, 10) || this.state.similarSeriesId;
39+
40+
axios.post(
41+
this.props.url,
42+
{
43+
'seriesId': this.state.seriesId,
44+
'similarSeriesId': similarSeriesId
45+
},
46+
{
47+
headers: {
48+
[this.props.csrfHeaderName]: this.props.csrfTokenValue,
49+
'Cache-Control': 'no-store'
50+
},
51+
validateStatus: (status) => {
52+
return status == 204 || status == 400;
53+
}
54+
}
55+
).then(response => {
56+
const data = response.data;
57+
if (data.hasOwnProperty('fieldErrors')) {
58+
const fieldErrors = [];
59+
if (data.fieldErrors.seriesId) {
60+
fieldErrors.push(...data.fieldErrors.seriesId);
61+
}
62+
if (data.fieldErrors.similarSeriesId) {
63+
fieldErrors.push(...data.fieldErrors.similarSeriesId);
64+
}
65+
this.setState({ isDisabled: false, validationErrors: fieldErrors });
66+
return;
67+
}
68+
69+
// no need to reset the state as page will be reloaded
70+
window.location.reload();
71+
72+
}).catch(error => {
73+
console.error(error);
74+
this.setState({ isDisabled: false, hasServerError: true });
75+
});
76+
}
77+
78+
render() {
79+
const hasValidationErrors = this.state.validationErrors.length > 0;
80+
81+
return (
82+
<div className="row">
83+
<div id="mark-similar-series-failed-msg"
84+
className={`alert alert-danger text-center col-sm-8 col-sm-offset-2 ${this.state.hasServerError ? '' : 'hidden'}`}>
85+
{ this.props.l10n['t_server_error'] || 'Server error' }
86+
</div>
87+
88+
<div className="col-sm-9 col-sm-offset-3">
89+
<form id="mark-similar-series-form"
90+
className={`form-inline ${hasValidationErrors ? 'has-error' : ''}`}
91+
onSubmit={this.handleSubmit}>
92+
93+
<div className="form-group form-group-sm">
94+
<input id="similar-id"
95+
type="text"
96+
className="form-control"
97+
required="required"
98+
placeholder={ this.props.l10n['t_similar_series_id'] || 'Similar series ID' }
99+
value={this.state.similarSeriesId}
100+
onChange={this.handleChange}
101+
disabled={this.state.isDisabled} />
102+
</div>
103+
104+
<div className="form-group form-group-sm">
105+
<button type="submit"
106+
className="btn btn-primary btn-sm"
107+
disabled={this.state.isDisabled}>
108+
{ this.props.l10n['t_mark_as_similar'] || 'Mark as similar' }
109+
</button>
110+
</div>
111+
<span id="similar-id.errors" className={`help-block ${hasValidationErrors ? '' : 'hidden'}`}>
112+
{ this.state.validationErrors.join(', ') }
113+
</span>
114+
115+
</form>
116+
</div>
117+
</div>
118+
)
119+
}
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
public interface AddSimilarSeriesDto {
21+
Integer getSeriesId();
22+
Integer getSimilarSeriesId();
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.Getter;
21+
import lombok.Setter;
22+
23+
import javax.validation.constraints.NotNull;
24+
25+
// @todo #1280 AddSimilarSeriesForm: series and similar series must be different
26+
@Getter
27+
@Setter
28+
public class AddSimilarSeriesForm implements AddSimilarSeriesDto {
29+
30+
// @todo #1280 AddSimilarSeriesForm: seriesId must exist
31+
@NotNull
32+
private Integer seriesId;
33+
34+
// @todo #1280 AddSimilarSeriesForm: add integration test for mandatory similarSeriesId
35+
// @todo #1280 AddSimilarSeriesForm: similarSeriesId must exist
36+
@NotNull
37+
private Integer similarSeriesId;
38+
}

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

+18
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ public class JdbcSeriesDao implements SeriesDao {
9393
@Value("${series.find_quantity_by_id}")
9494
private String findQuantityByIdSql;
9595

96+
@Value("${series.add_similar_series}")
97+
private String addSimilarSeriesSql;
98+
9699
@Override
97100
public Integer add(AddSeriesDbDto series) {
98101
Map<String, Object> params = new HashMap<>();
@@ -291,4 +294,19 @@ public Integer findQuantityById(Integer seriesId) {
291294
}
292295
}
293296

297+
@Override
298+
public void markAsSimilar(Integer seriesId, Integer similarSeriesId) {
299+
Map<String, Object> params = new HashMap<>();
300+
params.put("series_id", seriesId);
301+
params.put("similar_series_id", similarSeriesId);
302+
303+
int affected = jdbcTemplate.update(addSimilarSeriesSql, params);
304+
305+
Validate.validState(
306+
affected == 1,
307+
"Unexpected number of affected rows after adding similar series: %d",
308+
affected
309+
);
310+
}
311+
294312
}

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

+29
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919

2020
import lombok.RequiredArgsConstructor;
2121
import org.apache.commons.lang3.StringUtils;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
2224
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
25+
import org.springframework.http.HttpStatus;
26+
import org.springframework.http.ResponseEntity;
2327
import org.springframework.security.core.annotation.AuthenticationPrincipal;
2428
import org.springframework.stereotype.Controller;
2529
import org.springframework.ui.Model;
@@ -30,7 +34,9 @@
3034
import org.springframework.web.bind.annotation.InitBinder;
3135
import org.springframework.web.bind.annotation.PathVariable;
3236
import org.springframework.web.bind.annotation.PostMapping;
37+
import org.springframework.web.bind.annotation.RequestBody;
3338
import org.springframework.web.bind.annotation.RequestParam;
39+
import org.springframework.web.bind.annotation.ResponseBody;
3440
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
3541
import ru.mystamps.web.common.EntityWithParentDto;
3642
import ru.mystamps.web.common.LinkEntityDto;
@@ -78,6 +84,8 @@
7884
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.TooManyMethods", "PMD.GodClass" })
7985
public class SeriesController {
8086

87+
private static final Logger LOG = LoggerFactory.getLogger(SeriesController.class);
88+
8189
private static final Integer CURRENT_YEAR;
8290
private static final Map<Integer, Integer> YEARS;
8391

@@ -553,6 +561,27 @@ public String searchSeriesByCatalog(
553561
return "series/search_result";
554562
}
555563

564+
// @todo #1280 Mark similar series: gracefully handle error when value mismatches to type
565+
@PostMapping(SeriesUrl.MARK_SIMILAR_SERIES)
566+
@ResponseBody
567+
public ResponseEntity<Void> markSimilarSeries(
568+
@RequestBody @Valid AddSimilarSeriesForm form) {
569+
570+
try {
571+
seriesService.markAsSimilar(form);
572+
return ResponseEntity.noContent().build();
573+
574+
} catch (RuntimeException ex) { // NOPMD: AvoidCatchingGenericException; try to catch-all
575+
LOG.error(
576+
"Couldn't mark series #{} similar to #{}: {}",
577+
form.getSeriesId(),
578+
form.getSimilarSeriesId(),
579+
ex.getMessage()
580+
);
581+
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
582+
}
583+
}
584+
556585
// "public" in order to be accessible from SeriesImportController
557586
public void addCategoriesToModel(Model model, String lang) {
558587
List<EntityWithParentDto> categories = categoryService.findCategoriesWithParents(lang);

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

+2
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,6 @@ public interface SeriesDao {
4141
long countUpdatedSince(Date date);
4242

4343
Integer findQuantityById(Integer seriesId);
44+
45+
void markAsSimilar(Integer seriesId, Integer similarSeriesId);
4446
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,6 @@ public interface SeriesService {
4848
List<SitemapInfoDto> findAllForSitemap();
4949

5050
List<PurchaseAndSaleDto> findPurchasesAndSales(Integer seriesId);
51+
52+
void markAsSimilar(AddSimilarSeriesForm dto);
5153
}

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

+18
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,24 @@ public List<PurchaseAndSaleDto> findPurchasesAndSales(Integer seriesId) {
301301
return seriesDao.findPurchasesAndSales(seriesId);
302302
}
303303

304+
// @todo #1280 SeriesServiceImpl.markAsSimilar(): add unit tests
305+
@Override
306+
@Transactional
307+
@PreAuthorize(HasAuthority.MARK_SIMILAR_SERIES)
308+
public void markAsSimilar(AddSimilarSeriesForm dto) {
309+
Validate.isTrue(dto != null, "DTO must be non null");
310+
311+
Integer seriesId = dto.getSeriesId();
312+
Validate.isTrue(seriesId != null, "Series id must be non null");
313+
314+
Integer similarSeriesId = dto.getSimilarSeriesId();
315+
Validate.isTrue(similarSeriesId != null, "Similar series id must be non null");
316+
317+
seriesDao.markAsSimilar(seriesId, similarSeriesId);
318+
319+
log.info("Series #{} has been marked as similar to #{}", seriesId, similarSeriesId);
320+
}
321+
304322
private List<SeriesInfoDto> findByCatalogNumber(
305323
StampsCatalogService catalogService,
306324
String number, String lang) {

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

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public final class SeriesUrl {
3535
public static final String INFO_CATEGORY_PAGE = "/category/{slug}";
3636
public static final String INFO_COUNTRY_PAGE = "/country/{slug}";
3737
public static final String ADD_IMAGE_SERIES_PAGE = "/series/{id}/image";
38+
public static final String MARK_SIMILAR_SERIES = "/series/similar";
3839
public static final String SERIES_INFO_PAGE_REGEXP = "/series/(\\d+|\\d+/(ask|image))";
3940
static final String SEARCH_SERIES_BY_CATALOG = "/series/search/by_catalog";
4041

@@ -48,6 +49,7 @@ public static void exposeUrlsToView(Map<String, String> urls) {
4849
urls.put("INFO_CATEGORY_PAGE", INFO_CATEGORY_PAGE);
4950
urls.put("INFO_COUNTRY_PAGE", INFO_COUNTRY_PAGE);
5051
urls.put("INFO_SERIES_PAGE", INFO_SERIES_PAGE);
52+
urls.put("MARK_SIMILAR_SERIES", MARK_SIMILAR_SERIES);
5153
urls.put("SEARCH_SERIES_BY_CATALOG", SEARCH_SERIES_BY_CATALOG);
5254
}
5355

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

+14-12
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,21 @@ 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.0";
35+
public static final String RESOURCES_VERSION = "v0.4.3.1";
3636

37-
// CheckStyle: ignore LineLength for next 10 lines
38-
private static final String CATALOG_UTILS_JS = "/public/js/" + RESOURCES_VERSION + "/CatalogUtils.min.js";
39-
private static final String COLLECTION_INFO_JS = "/public/js/" + RESOURCES_VERSION + "/collection/info.min.js";
40-
private static final String DATE_UTILS_JS = "/public/js/" + RESOURCES_VERSION + "/DateUtils.min.js";
41-
private static final String MAIN_CSS = "/static/" + RESOURCES_VERSION + "/styles/main.min.css";
42-
private static final String PARTICIPANT_ADD_JS = "/public/js/" + RESOURCES_VERSION + "/participant/add.min.js";
43-
private static final String SERIES_ADD_JS = "/public/js/" + RESOURCES_VERSION + "/series/add.min.js";
44-
private static final String SERIES_INFO_JS = "/public/js/" + RESOURCES_VERSION + "/series/info.min.js";
37+
// CheckStyle: ignore LineLength for next 11 lines
38+
private static final String CATALOG_UTILS_JS = "/public/js/" + RESOURCES_VERSION + "/CatalogUtils.min.js";
39+
private static final String COLLECTION_INFO_JS = "/public/js/" + RESOURCES_VERSION + "/collection/info.min.js";
40+
private static final String DATE_UTILS_JS = "/public/js/" + RESOURCES_VERSION + "/DateUtils.min.js";
41+
private static final String MAIN_CSS = "/static/" + RESOURCES_VERSION + "/styles/main.min.css";
42+
private static final String PARTICIPANT_ADD_JS = "/public/js/" + RESOURCES_VERSION + "/participant/add.min.js";
43+
private static final String SERIES_ADD_JS = "/public/js/" + RESOURCES_VERSION + "/series/add.min.js";
44+
private static final String SERIES_INFO_JS = "/public/js/" + RESOURCES_VERSION + "/series/info.min.js";
4545
// @todo #1057 Use minified version of SeriesSaleImportForm.js
46-
private static final String SALE_IMPORT_FORM_JS = "/public/js/" + RESOURCES_VERSION + "/components/SeriesSaleImportForm.js";
47-
private static final String BOOTSTRAP_LANGUAGE = "https://cdn.jsdelivr.net/gh/usrz/bootstrap-languages@3ac2a3d2b27ac43a471cd99e79d378a03b2c6b5f/languages.min.css";
48-
private static final String FAVICON_ICO = "/favicon.ico";
46+
private static final String SALE_IMPORT_FORM_JS = "/public/js/" + RESOURCES_VERSION + "/components/SeriesSaleImportForm.js";
47+
private static final String SIMILAR_SERIES_FORM_JS = "/public/js/" + RESOURCES_VERSION + "/components/SimilarSeriesForm.js";
48+
private static final String BOOTSTRAP_LANGUAGE = "https://cdn.jsdelivr.net/gh/usrz/bootstrap-languages@3ac2a3d2b27ac43a471cd99e79d378a03b2c6b5f/languages.min.css";
49+
private static final String FAVICON_ICO = "/favicon.ico";
4950

5051
// see also pom.xml and MvcConfig.addResourceHandlers()
5152
private static final String AXIOS_JS = "0.19.2/dist/axios.min.js";
@@ -78,6 +79,7 @@ public static void exposeResourcesToView(Map<String, String> resources, String h
7879
put(resources, host, "SERIES_ADD_JS", SERIES_ADD_JS);
7980
put(resources, host, "SERIES_INFO_JS", SERIES_INFO_JS);
8081
put(resources, host, "SALE_IMPORT_FORM_JS", SALE_IMPORT_FORM_JS);
82+
put(resources, host, "SIMILAR_SERIES_FORM_JS", SIMILAR_SERIES_FORM_JS);
8183
}
8284

8385
// see also MvcConfig.addResourceHandlers()

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

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public final class Authority {
3535
public static final GrantedAuthority IMPORT_SERIES = new SimpleGrantedAuthority(StringAuthority.IMPORT_SERIES);
3636
public static final GrantedAuthority IMPORT_SERIES_SALES = new SimpleGrantedAuthority(StringAuthority.IMPORT_SERIES_SALES);
3737
public static final GrantedAuthority MANAGE_TOGGLZ = new SimpleGrantedAuthority(StringAuthority.MANAGE_TOGGLZ);
38+
public static final GrantedAuthority MARK_SIMILAR_SERIES = new SimpleGrantedAuthority(StringAuthority.MARK_SIMILAR_SERIES);
3839
public static final GrantedAuthority UPDATE_COLLECTION = new SimpleGrantedAuthority(StringAuthority.UPDATE_COLLECTION);
3940
public static final GrantedAuthority VIEW_ANY_ESTIMATION = new SimpleGrantedAuthority(StringAuthority.VIEW_ANY_ESTIMATION);
4041
public static final GrantedAuthority VIEW_DAILY_STATS = new SimpleGrantedAuthority(StringAuthority.VIEW_DAILY_STATS);

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

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ private static Collection<? extends GrantedAuthority> getAuthorities(UserDetails
8080
authorities.add(Authority.IMPORT_SERIES);
8181
authorities.add(Authority.IMPORT_SERIES_SALES);
8282
authorities.add(Authority.MANAGE_TOGGLZ);
83+
authorities.add(Authority.MARK_SIMILAR_SERIES);
8384
authorities.add(Authority.VIEW_ANY_ESTIMATION);
8485
authorities.add(Authority.VIEW_DAILY_STATS);
8586
authorities.add(Authority.VIEW_SERIES_SALES);

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

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public final class HasAuthority {
3636
public static final String CREATE_SERIES = "hasAuthority('" + StringAuthority.CREATE_SERIES + "')";
3737
public static final String DOWNLOAD_IMAGE = "hasAuthority('" + StringAuthority.DOWNLOAD_IMAGE + "')";
3838
public static final String IMPORT_SERIES = "hasAuthority('" + StringAuthority.IMPORT_SERIES + "')";
39+
public static final String MARK_SIMILAR_SERIES = "hasAuthority('" + StringAuthority.MARK_SIMILAR_SERIES + "')";
3940
public static final String UPDATE_COLLECTION = "hasAuthority('" + StringAuthority.UPDATE_COLLECTION + "')";
4041
public static final String VIEW_DAILY_STATS = "hasAuthority('" + StringAuthority.VIEW_DAILY_STATS + "')";
4142
public static final String VIEW_SERIES_SALES = "hasAuthority('" + StringAuthority.VIEW_SERIES_SALES + "')";

0 commit comments

Comments
 (0)