Skip to content

Commit f96d776

Browse files
committed
Merge branch 'gh517_country_prediction'
Implement country prediction when user adds a series. Addressed to #517
2 parents f26b8c2 + 2579872 commit f96d776

File tree

14 files changed

+198
-7
lines changed

14 files changed

+198
-7
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public final class Url {
5252
public static final String ADD_IMAGE_SERIES_PAGE = "/series/{id}/image";
5353
public static final String SEARCH_SERIES_BY_CATALOG = "/series/search/by_catalog";
5454

55+
public static final String SUGGEST_SERIES_COUNTRY = "/suggest/series_country";
56+
5557
public static final String ADD_CATEGORY_PAGE = "/category/add";
5658
public static final String LIST_CATEGORIES_PAGE = "/category/list";
5759
public static final String INFO_CATEGORY_PAGE = "/category/{slug}";
@@ -77,7 +79,7 @@ public final class Url {
7779
public static final String ADD_SERIES_WITH_COUNTRY_PAGE = "/series/add/country/{slug}";
7880

7981
// MUST be updated when any of our resources were modified
80-
public static final String RESOURCES_VERSION = "v0.3.0";
82+
public static final String RESOURCES_VERSION = "v0.3.1";
8183

8284
// CheckStyle: ignore LineLength for next 4 lines
8385
public static final String MAIN_CSS = "/static/" + RESOURCES_VERSION + "/styles/main.min.css";
@@ -125,6 +127,7 @@ public static Map<String, String> asMap(boolean serveContentFromSingleHost) {
125127
map.put("INFO_SERIES_PAGE", INFO_SERIES_PAGE);
126128
map.put("ADD_IMAGE_SERIES_PAGE", ADD_IMAGE_SERIES_PAGE);
127129
map.put("SEARCH_SERIES_BY_CATALOG", SEARCH_SERIES_BY_CATALOG);
130+
map.put("SUGGEST_SERIES_COUNTRY", SUGGEST_SERIES_COUNTRY);
128131
map.put("ADD_CATEGORY_PAGE", ADD_CATEGORY_PAGE);
129132
map.put("INFO_CATEGORY_PAGE", INFO_CATEGORY_PAGE);
130133
map.put("LIST_CATEGORIES_PAGE", LIST_CATEGORIES_PAGE);

src/main/java/ru/mystamps/web/config/ControllersConfig.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,5 +110,12 @@ public SiteController getSiteController() {
110110
public SitemapController getSitemapController() {
111111
return new SitemapController(servicesConfig.getSeriesService());
112112
}
113-
113+
114+
@Bean
115+
public SuggestionController getSuggestionController() {
116+
return new SuggestionController(
117+
servicesConfig.getCountryService()
118+
);
119+
}
120+
114121
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (C) 2009-2017 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.controller;
19+
20+
import org.springframework.stereotype.Controller;
21+
import org.springframework.web.bind.annotation.GetMapping;
22+
import org.springframework.web.bind.annotation.ResponseBody;
23+
24+
import lombok.RequiredArgsConstructor;
25+
26+
import ru.mystamps.web.Url;
27+
import ru.mystamps.web.controller.converter.annotation.CurrentUser;
28+
import ru.mystamps.web.service.CountryService;
29+
30+
@Controller
31+
@RequiredArgsConstructor
32+
public class SuggestionController {
33+
34+
private final CountryService countryService;
35+
36+
/**
37+
* @author John Shkarin
38+
*/
39+
@ResponseBody
40+
@GetMapping(Url.SUGGEST_SERIES_COUNTRY)
41+
public String suggestCountryForUser(@CurrentUser Integer currentUserId) {
42+
return countryService.suggestCountryForUser(currentUserId);
43+
}
44+
45+
}
46+

src/main/java/ru/mystamps/web/dao/CountryDao.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import ru.mystamps.web.dao.dto.AddCountryDbDto;
2424
import ru.mystamps.web.dao.dto.LinkEntityDto;
2525

26+
@SuppressWarnings("PMD.TooManyMethods")
2627
public interface CountryDao {
2728
Integer add(AddCountryDbDto country);
2829
long countAll();
@@ -35,4 +36,6 @@ public interface CountryDao {
3536
List<Object[]> getStatisticsOf(Integer collectionId, String lang);
3637
List<LinkEntityDto> findAllAsLinkEntities(String lang);
3738
LinkEntityDto findOneAsLinkEntity(String slug, String lang);
39+
String findCountryOfLastCreatedSeriesByUser(Integer userId);
40+
String findPopularCountryInCollection(Integer userId);
3841
}

src/main/java/ru/mystamps/web/dao/impl/JdbcCountryDao.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
import ru.mystamps.web.dao.dto.LinkEntityDto;
4040

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

4545
private final NamedParameterJdbcTemplate jdbcTemplate;
@@ -76,6 +76,14 @@ public class JdbcCountryDao implements CountryDao {
7676

7777
@Value("${country.find_country_link_info_by_slug}")
7878
private String findCountryLinkEntityBySlugSql;
79+
80+
@SuppressWarnings("PMD.LongVariable")
81+
@Value("${country.find_from_last_created_series_by_user}")
82+
private String findFromLastCreatedSeriesByUserSql;
83+
84+
@SuppressWarnings("PMD.LongVariable")
85+
@Value("${country.find_popular_country_from_user_collection}")
86+
private String findPopularCountryInCollectionSql;
7987

8088
@Override
8189
public Integer add(AddCountryDbDto country) {
@@ -206,5 +214,38 @@ public LinkEntityDto findOneAsLinkEntity(String slug, String lang) {
206214
return null;
207215
}
208216
}
217+
218+
/**
219+
* @author Shkarin John
220+
* @author Slava Semushin
221+
*/
222+
@Override
223+
public String findCountryOfLastCreatedSeriesByUser(Integer userId) {
224+
try {
225+
return jdbcTemplate.queryForObject(
226+
findFromLastCreatedSeriesByUserSql,
227+
Collections.singletonMap("created_by", userId),
228+
String.class
229+
);
230+
} catch (EmptyResultDataAccessException ignored) {
231+
return null;
232+
}
233+
}
234+
235+
/**
236+
* @author Shkarin John
237+
*/
238+
@Override
239+
public String findPopularCountryInCollection(Integer userId) {
240+
try {
241+
return jdbcTemplate.queryForObject(
242+
findPopularCountryInCollectionSql,
243+
Collections.singletonMap("user_id", userId),
244+
String.class
245+
);
246+
} catch (EmptyResultDataAccessException ignored) {
247+
return null;
248+
}
249+
}
209250

210251
}

src/main/java/ru/mystamps/web/service/CountryService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import ru.mystamps.web.dao.dto.LinkEntityDto;
2424
import ru.mystamps.web.service.dto.AddCountryDto;
2525

26+
@SuppressWarnings("PMD.TooManyMethods")
2627
public interface CountryService {
2728
String add(AddCountryDto dto, Integer userId);
2829
List<LinkEntityDto> findAllAsLinkEntities(String lang);
@@ -35,4 +36,5 @@ public interface CountryService {
3536
long countAddedSince(Date date);
3637
long countUntranslatedNamesSince(Date date);
3738
List<Object[]> getStatisticsOf(Integer collectionId, String lang);
39+
String suggestCountryForUser(Integer userId);
3840
}

src/main/java/ru/mystamps/web/service/CountryServiceImpl.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import ru.mystamps.web.util.SlugUtils;
4343

4444
@RequiredArgsConstructor
45+
@SuppressWarnings("PMD.TooManyMethods")
4546
public class CountryServiceImpl implements CountryService {
4647
private static final Logger LOG = LoggerFactory.getLogger(CountryServiceImpl.class);
4748

@@ -162,5 +163,41 @@ public List<Object[]> getStatisticsOf(Integer collectionId, String lang) {
162163

163164
return countryDao.getStatisticsOf(collectionId, lang);
164165
}
165-
166+
167+
/**
168+
* @author Shkarin John
169+
* @author Slava Semushin
170+
*/
171+
@Override
172+
@Transactional(readOnly = true)
173+
@PreAuthorize(HasAuthority.CREATE_SERIES)
174+
public String suggestCountryForUser(Integer userId) {
175+
Validate.isTrue(userId != null, "User id must be non null");
176+
177+
// if user created a series with a country, let's suggest this country to him
178+
String slug = countryDao.findCountryOfLastCreatedSeriesByUser(userId);
179+
if (slug != null) {
180+
LOG.info(
181+
"Country {} has been suggested to user #{} from a recently created series",
182+
slug,
183+
userId
184+
);
185+
return slug;
186+
}
187+
188+
// user has never created a country but most of the series in his collection
189+
// belong to a specific country, let's suggest this country to him
190+
slug = countryDao.findPopularCountryInCollection(userId);
191+
if (slug != null) {
192+
LOG.info(
193+
"Country {} has been suggested to user #{} as popular in his collection",
194+
slug,
195+
userId
196+
);
197+
}
198+
199+
LOG.info("Couldn't suggest a country for user #{}", userId);
200+
201+
return slug;
202+
}
166203
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ protected void configure(HttpSecurity http) throws Exception {
7575
.mvcMatchers(Url.ADD_COUNTRY_PAGE).hasAuthority(StringAuthority.CREATE_COUNTRY)
7676
.mvcMatchers(Url.ADD_SERIES_PAGE).hasAuthority(StringAuthority.CREATE_SERIES)
7777
.mvcMatchers(Url.SITE_EVENTS_PAGE).hasAuthority(StringAuthority.VIEW_SITE_EVENTS)
78+
.mvcMatchers(Url.SUGGEST_SERIES_COUNTRY).hasAuthority(StringAuthority.CREATE_SERIES)
7879
.regexMatchers(HttpMethod.POST, "/series/[0-9]+")
7980
.hasAnyAuthority(
8081
StringAuthority.UPDATE_COLLECTION,

src/main/java/ru/mystamps/web/support/togglz/Features.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ public enum Features implements Feature {
7474

7575
@Label("View site events")
7676
@EnabledByDefault
77-
VIEW_SITE_EVENTS;
77+
VIEW_SITE_EVENTS,
78+
79+
@Label("/series/add: show link with auto-suggestions")
80+
@EnabledByDefault
81+
SHOW_SUGGESTION_LINK;
7882

7983
public boolean isActive() {
8084
return FeatureContext.getFeatureManager().isActive(this);

src/main/javascript/series/add.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// IMPORTANT:
33
// You have to update Url.RESOURCES_VERSION each time whenever you're modified this file!
44
//
5-
function initPage() {
5+
function initPage(suggestCountryUrl) {
66
$('#country').selectize();
77

88
$('.js-catalog-numbers').on('blur', function() {
@@ -21,4 +21,22 @@ function initPage() {
2121
$('.js-with-tooltip').tooltip({
2222
'placement': 'right'
2323
});
24+
25+
if (suggestCountryUrl != null) {
26+
$.get(suggestCountryUrl, function handleSuggestedCountry(slug) {
27+
if (slug == "") {
28+
return;
29+
}
30+
31+
var country = $("#js-guess-country-link");
32+
country.click(function chooseSuggestedCountry() {
33+
$(this).hide();
34+
35+
var select_country = $("#country").selectize();
36+
var selectize = select_country[0].selectize;
37+
selectize.setValue(slug);
38+
});
39+
country.show();
40+
});
41+
}
2442
}

src/main/resources/ru/mystamps/i18n/Messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ t_add_more_images_hint = Later you will be able to add additional images
121121
t_not_chosen = Not chosen
122122
t_create_category_hint = You can also <a tabindex="-1" href="{0}">add a new category</a>
123123
t_create_country_hint = You can also <a tabindex="-1" href="{0}">add a new country</a>
124+
t_guess_country = Guess a country
124125

125126
# series/info.html
126127
t_series_info = Info about series

src/main/resources/ru/mystamps/i18n/Messages_ru.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ t_add_more_images_hint = Вы сможете добавить дополните
121121
t_not_chosen = Не выбрана
122122
t_create_category_hint = Вы также можете <a tabindex="-1" href="{0}">добавить новую категорию</a>
123123
t_create_country_hint = Вы также можете <a tabindex="-1" href="{0}">добавить новую страну</a>
124+
t_guess_country = Угадать страну
124125

125126
# series/info.html
126127
t_series_info = Информация о серии

src/main/resources/sql/country_dao_queries.properties

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,26 @@ country.find_country_link_info_by_slug = \
8181
FROM countries c \
8282
WHERE c.slug = :slug \
8383
ORDER BY CASE WHEN 'ru' = :lang THEN COALESCE(c.name_ru, c.name) ELSE c.name END
84+
85+
country.find_from_last_created_series_by_user = \
86+
SELECT c.slug \
87+
FROM series s \
88+
JOIN countries c \
89+
ON c.id = s.country_id \
90+
WHERE s.created_by = :created_by \
91+
ORDER BY s.created_at DESC \
92+
LIMIT 1
93+
94+
country.find_popular_country_from_user_collection = \
95+
SELECT co.slug \
96+
FROM collections c \
97+
JOIN collections_series cs \
98+
ON c.id = cs.collection_id \
99+
JOIN series s \
100+
ON s.id = cs.series_id \
101+
JOIN countries co \
102+
ON co.id = s.country_id \
103+
WHERE c.user_id = :user_id \
104+
GROUP BY co.id \
105+
ORDER BY COUNT(*) DESC \
106+
LIMIT 1

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<link rel="stylesheet" href="../../static/styles/main.css" th:href="${MAIN_CSS}" />
1616
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.3/css/selectize.bootstrap3.min.css" th:href="${SELECTIZE_CSS}" />
1717
</head>
18-
<body onload="initPage()">
18+
<body th:onload="|initPage('@{${SUGGEST_SERIES_COUNTRY}}')|" onload="initPage(null)">
1919
<div class="container-fluid">
2020
<div class="row" id="header">
2121
<div id="logo" class="col-sm-9 vcenter">
@@ -202,6 +202,10 @@ <h3 th:text="${#strings.capitalize(add_series)}">
202202
<span id="country.errors" class="help-block" th:if="${#fields.hasErrors('country')}" th:each="error : ${#fields.errors('country')}" th:text="${error}"></span>
203203
/*/-->
204204
</div>
205+
206+
<small togglz:active="SHOW_SUGGESTION_LINK">
207+
<a tabindex="-1" th:text="#{t_guess_country}" href="javascript:void(0)" id="js-guess-country-link" th:style="'display: none; position: relative; top: 7px;'">Guess a country</a>
208+
</small>
205209
</div>
206210

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

0 commit comments

Comments
 (0)