Skip to content

Commit 2b60868

Browse files
committed
feat(/series/add): suggest a category to a user.
Addressed to #517
1 parent 008442a commit 2b60868

File tree

14 files changed

+188
-7
lines changed

14 files changed

+188
-7
lines changed

NEWS.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
- (functionality) user may specify how many stamps from a series in his/her collection
2424
- (functionality) paid users may specify a price that he/she paid for a series
2525
- (infrastructure) use WireMock in integration tests for mocking external services
26+
- (functionality) suggest a possible country on a series creation page (contributed by John Shkarin)
27+
- (functionality) suggest a possible category on a series creation page
2628

2729
0.3
2830
- (functionality) implemented possibility to user to add series to his collection

src/main/java/ru/mystamps/web/Url.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public final class Url {
5757
public static final String REQUEST_IMPORT_PAGE = "/series/import/request/{id}";
5858
public static final String LIST_IMPORT_REQUESTS_PAGE = "/series/import/requests";
5959

60+
public static final String SUGGEST_SERIES_CATEGORY = "/suggest/series_category";
6061
public static final String SUGGEST_SERIES_COUNTRY = "/suggest/series_country";
6162

6263
public static final String ADD_CATEGORY_PAGE = "/category/add";
@@ -90,7 +91,7 @@ public final class Url {
9091
public static final String ADD_SERIES_WITH_COUNTRY_PAGE = "/series/add/country/{slug}";
9192

9293
// MUST be updated when any of our resources were modified
93-
public static final String RESOURCES_VERSION = "v0.3.10";
94+
public static final String RESOURCES_VERSION = "v0.3.11";
9495

9596
// CheckStyle: ignore LineLength for next 7 lines
9697
public static final String MAIN_CSS = "/static/" + RESOURCES_VERSION + "/styles/main.min.css";
@@ -163,6 +164,7 @@ public static Map<String, String> asMap(boolean production) {
163164
map.put("SEARCH_SERIES_BY_CATALOG", SEARCH_SERIES_BY_CATALOG);
164165
map.put("SITE_EVENTS_PAGE", SITE_EVENTS_PAGE);
165166
map.put("SUGGEST_SERIES_COUNTRY", SUGGEST_SERIES_COUNTRY);
167+
map.put("SUGGEST_SERIES_CATEGORY", SUGGEST_SERIES_CATEGORY);
166168

167169
if (serveContentFromSingleHost) {
168170
// Constants sorted in an ascending order.

src/main/java/ru/mystamps/web/feature/category/CategoryConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public CategoryController categoryController() {
4747
return new CategoryController(categoryService, seriesService);
4848
}
4949

50+
@Bean
51+
public SuggestionController suggestionCategoryController() {
52+
return new SuggestionController(categoryService);
53+
}
54+
5055
}
5156

5257
@RequiredArgsConstructor

src/main/java/ru/mystamps/web/feature/category/CategoryDao.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ public interface CategoryDao {
3939
List<LinkEntityDto> findAllAsLinkEntities(String lang);
4040
LinkEntityDto findOneAsLinkEntity(String slug, String lang);
4141
List<EntityWithParentDto> findCategoriesWithParents(String lang);
42+
String findCategoryOfLastCreatedSeriesByUser(Integer userId);
4243
}

src/main/java/ru/mystamps/web/feature/category/CategoryService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ public interface CategoryService {
3939
long countAddedSince(Date date);
4040
long countUntranslatedNamesSince(Date date);
4141
List<Object[]> getStatisticsOf(Integer collectionId, String lang);
42+
String suggestCategoryForUser(Integer userId);
4243
}

src/main/java/ru/mystamps/web/feature/category/CategoryServiceImpl.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,28 @@ public List<Object[]> getStatisticsOf(Integer collectionId, String lang) {
205205
return categoryDao.getStatisticsOf(collectionId, lang);
206206
}
207207

208+
// @todo #517 Add integration tests for category suggestion
209+
@Override
210+
@Transactional(readOnly = true)
211+
@PreAuthorize(HasAuthority.CREATE_SERIES)
212+
public String suggestCategoryForUser(Integer userId) {
213+
Validate.isTrue(userId != null, "User id must be non null");
214+
215+
// if user created a series with a category, let's suggest this category to him
216+
String slug = categoryDao.findCategoryOfLastCreatedSeriesByUser(userId);
217+
if (slug != null) {
218+
log.debug(
219+
"Category {} has been suggested to user #{} from a recently created series",
220+
slug,
221+
userId
222+
);
223+
return slug;
224+
}
225+
226+
// @todo #517: CategoryService.suggestCategoryForUser(): suggest a last created category
227+
// @todo #517: CategoryService.suggestCategoryForUser(): suggest the most popular category
228+
229+
return null;
230+
}
231+
208232
}

src/main/java/ru/mystamps/web/feature/category/JdbcCategoryDao.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
import ru.mystamps.web.support.jdbc.RowMappers;
4040

4141
@RequiredArgsConstructor
42-
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.TooManyMethods" })
42+
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.TooManyMethods", "PMD.TooManyFields" })
4343
public class JdbcCategoryDao implements CategoryDao {
4444

4545
private final NamedParameterJdbcTemplate jdbcTemplate;
@@ -87,6 +87,10 @@ public class JdbcCategoryDao implements CategoryDao {
8787
@Value("${category.find_categories_with_parent_names}")
8888
private String findCategoriesWithParentNamesSql;
8989

90+
@SuppressWarnings("PMD.LongVariable")
91+
@Value("${category.find_from_last_created_series_by_user}")
92+
private String findFromLastCreatedSeriesByUserSql;
93+
9094
@Override
9195
public Integer add(AddCategoryDbDto category) {
9296
Map<String, Object> params = new HashMap<>();
@@ -240,4 +244,17 @@ public List<EntityWithParentDto> findCategoriesWithParents(String lang) {
240244
);
241245
}
242246

247+
@Override
248+
public String findCategoryOfLastCreatedSeriesByUser(Integer userId) {
249+
try {
250+
return jdbcTemplate.queryForObject(
251+
findFromLastCreatedSeriesByUserSql,
252+
Collections.singletonMap("created_by", userId),
253+
String.class
254+
);
255+
} catch (EmptyResultDataAccessException ignored) {
256+
return null;
257+
}
258+
}
259+
243260
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (C) 2009-2018 Slava Semushin <slava.semushin@gmail.com>
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.category;
19+
20+
import org.apache.commons.lang3.StringUtils;
21+
22+
import org.springframework.web.bind.annotation.GetMapping;
23+
import org.springframework.web.bind.annotation.RestController;
24+
25+
import lombok.RequiredArgsConstructor;
26+
27+
import ru.mystamps.web.Url;
28+
import ru.mystamps.web.controller.converter.annotation.CurrentUser;
29+
30+
@RestController
31+
@RequiredArgsConstructor
32+
class SuggestionController {
33+
34+
private final CategoryService categoryService;
35+
36+
@GetMapping(Url.SUGGEST_SERIES_CATEGORY)
37+
public String suggestCategoryForUser(@CurrentUser Integer currentUserId) {
38+
return StringUtils.defaultString(
39+
categoryService.suggestCategoryForUser(currentUserId),
40+
StringUtils.EMPTY
41+
);
42+
}
43+
44+
}
45+

src/main/java/ru/mystamps/web/feature/country/CountryConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public CountryController countryController() {
4848
}
4949

5050
@Bean
51-
public SuggestionController suggestionController() {
51+
public SuggestionController suggestionCountryController() {
5252
return new SuggestionController(countryService);
5353
}
5454

src/main/javascript/series/add.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// You must update Url.RESOURCES_VERSION each time whenever you're modified this file!
44
//
55

6-
function initPage(suggestCountryUrl) {
6+
function initPage(suggestCategoryUrl, suggestCountryUrl) {
77
$('#country').selectize();
88

99
$('.js-catalog-numbers').on('blur', function expandCatalogNumbers() {
@@ -28,6 +28,26 @@ function initPage(suggestCountryUrl) {
2828
'placement': 'right'
2929
});
3030

31+
if (suggestCategoryUrl != null) {
32+
$.get(suggestCategoryUrl, function handleSuggestedCategory(slug) {
33+
if (slug == '') {
34+
return;
35+
}
36+
37+
var suggestCategoryLink = $('#js-suggest-category-link');
38+
suggestCategoryLink.click(function chooseSuggestedCategory() {
39+
suggestCategoryLink.addClass('hidden');
40+
chooseCategoryBySlug(slug);
41+
});
42+
43+
var categoryName = getCategoryNameBySlug(slug);
44+
var newText = suggestCategoryLink.text().replace('%name%', categoryName);
45+
suggestCategoryLink.text(newText);
46+
47+
suggestCategoryLink.removeClass('hidden');
48+
});
49+
}
50+
3151
if (suggestCountryUrl != null) {
3252
$.get(suggestCountryUrl, function handleSuggestedCountry(slug) {
3353
if (slug == '') {
@@ -49,6 +69,15 @@ function initPage(suggestCountryUrl) {
4969
}
5070
}
5171

72+
73+
function chooseCategoryBySlug(slug) {
74+
$('#category').val(slug);
75+
}
76+
77+
function getCategoryNameBySlug(slug) {
78+
return $('#category option[value="' + slug + '"]').text();
79+
}
80+
5281
function chooseCountryBySlug(slug) {
5382
var countrySelectBox = $('#country').selectize();
5483
var selectize = countrySelectBox[0].selectize;

src/main/resources/sql/category_dao_queries.properties

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,12 @@ LEFT JOIN top_categories t \
115115
THEN CONCAT(COALESCE(t.name_ru, t.name), COALESCE(c.name_ru, c.name)) \
116116
ELSE CONCAT(t.name, c.name) \
117117
END
118+
119+
category.find_from_last_created_series_by_user = \
120+
SELECT c.slug \
121+
FROM series s \
122+
JOIN categories c \
123+
ON c.id = s.category_id \
124+
WHERE s.created_by = :created_by \
125+
ORDER BY s.created_at DESC \
126+
LIMIT 1

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
<link rel="stylesheet" href="../../static/styles/main.css" th:href="${MAIN_CSS}" />
1717
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.4/css/selectize.bootstrap3.min.css" th:href="${SELECTIZE_CSS}" />
1818
</head>
19-
<body th:with="suggestCountryUrl=${addSeriesForm.country != null and addSeriesForm.country.slug != null ? 'null' : '''__@{${SUGGEST_SERIES_COUNTRY}}__'''}"
20-
th:onload="|initPage(${suggestCountryUrl})|"
21-
onload="initPage(null)">
19+
<body th:with="suggestCountryUrl=${addSeriesForm.country != null and addSeriesForm.country.slug != null ? 'null' : '''__@{${SUGGEST_SERIES_COUNTRY}}__'''},suggestCategoryUrl=${addSeriesForm.category != null and addSeriesForm.category.slug != null ? 'null' : '''__@{${SUGGEST_SERIES_CATEGORY}}__'''}"
20+
th:onload="|initPage(${suggestCategoryUrl}, ${suggestCountryUrl})|"
21+
onload="initPage(null, null)">
2222
<div class="container-fluid">
2323
<div class="row" id="header">
2424
<div id="logo" class="col-sm-9 vcenter">
@@ -138,6 +138,16 @@ <h3 th:text="${#strings.capitalize(add_series)}">
138138
<span id="category.errors" class="help-block" th:if="${#fields.hasErrors('category')}" th:each="error : ${#fields.errors('category')}" th:text="${error}"></span>
139139
/*/-->
140140
</div>
141+
142+
<small togglz:active="SHOW_SUGGESTION_LINK">
143+
<a tabindex="-1"
144+
th:text="#{t_pick_percent_name}"
145+
href="javascript:$('#js-suggest-category-link').hide(); chooseCategoryBySlug('sport'); void(0)"
146+
th:href="'javascript:void(0)'"
147+
id="js-suggest-category-link"
148+
class="link-vcenter"
149+
th:classappend="hidden">Pick "Sport"</a>
150+
</small>
141151
</div>
142152

143153
<div class="form-group form-group-sm" th:classappend="${#fields.hasErrors('country') ? 'has-error' : ''}">

src/test/groovy/ru/mystamps/web/feature/category/CategoryServiceImplTest.groovy

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,5 +453,37 @@ class CategoryServiceImplTest extends Specification {
453453
then:
454454
1 * categoryDao.getStatisticsOf(expectedCollectionId, expectedLang) >> null
455455
}
456+
457+
//
458+
// Tests for suggestCategoryForUser()
459+
//
460+
461+
def 'suggestCategoryForUser() should throw exception when user id is null'() {
462+
when:
463+
service.suggestCategoryForUser(null)
464+
then:
465+
thrown IllegalArgumentException
466+
}
467+
468+
def 'suggestCategoryForUser() should return category of the last created series'() {
469+
given:
470+
Integer expectedUserId = Random.userId()
471+
String expectedSlug = Random.categorySlug()
472+
when:
473+
String slug = service.suggestCategoryForUser(expectedUserId)
474+
then:
475+
1 * categoryDao.findCategoryOfLastCreatedSeriesByUser(expectedUserId) >> expectedSlug
476+
and:
477+
slug == expectedSlug
478+
}
479+
480+
def 'suggestCategoryForUser() should return null when cannot suggest'() {
481+
when:
482+
String slug = service.suggestCategoryForUser(Random.userId())
483+
then:
484+
1 * categoryDao.findCategoryOfLastCreatedSeriesByUser(_ as Integer) >> null
485+
and:
486+
slug == null
487+
}
456488

457489
}

src/test/java/ru/mystamps/web/tests/Random.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ public static String categoryName() {
147147
return name;
148148
}
149149

150+
public static String categorySlug() {
151+
return SlugUtils.slugify(categoryName());
152+
}
153+
150154
public static String countryName() {
151155
String name = between(
152156
ValidationRules.COUNTRY_NAME_MIN_LENGTH,

0 commit comments

Comments
 (0)