Skip to content

Commit c3e11aa

Browse files
committed
feat: users can add multiple instances of a series into a collection.
Fix #1123
1 parent a27ee65 commit c3e11aa

File tree

16 files changed

+223
-46
lines changed

16 files changed

+223
-46
lines changed

NEWS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
0.x (upcoming release)
2+
- (feature) users can add multiple instances of a series into a collection
23
- (infrastructure) all integration tests now pass on PostgreSQL
34
- (infrastructure) migrate unit tests from Hamcrest to AssertJ
45

src/main/java/ru/mystamps/web/feature/collection/CollectionDao.java

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import java.util.Date;
2323
import java.util.List;
24+
import java.util.Map;
2425

2526
@SuppressWarnings("PMD.TooManyMethods")
2627
public interface CollectionDao {
@@ -34,6 +35,7 @@ public interface CollectionDao {
3435
Integer add(AddCollectionDbDto collection);
3536
void markAsModified(Integer userId, Date updatedAt);
3637
boolean isSeriesInUserCollection(Integer userId, Integer seriesId);
38+
Map<Integer, Integer> findSeriesInstances(Integer userId, Integer seriesId);
3739
Integer addSeriesToUserCollection(AddToCollectionDbDto dto);
3840
void removeSeriesFromUserCollection(Integer userId, Integer seriesId);
3941
CollectionInfoDto findCollectionInfoBySlug(String slug);

src/main/java/ru/mystamps/web/feature/collection/CollectionService.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121

2222
import java.util.Date;
2323
import java.util.List;
24+
import java.util.Map;
2425

2526
@SuppressWarnings("PMD.TooManyMethods")
2627
public interface CollectionService {
2728
void createCollection(Integer ownerId, String ownerLogin);
2829
void addToCollection(Integer userId, AddToCollectionDto dto);
29-
void removeFromCollection(Integer userId, Integer seriesId);
30+
void removeFromCollection(Integer userId, Integer seriesId, Integer seriesInstanceId);
3031
boolean isSeriesInCollection(Integer userId, Integer seriesId);
32+
Map<Integer, Integer> findSeriesInstances(Integer userId, Integer seriesId);
3133
long countCollectionsOfUsers();
3234
long countUpdatedSince(Date date);
3335
long countSeriesOf(Integer collectionId);

src/main/java/ru/mystamps/web/feature/collection/CollectionServiceImpl.java

+28-4
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@
2929

3030
import java.util.Date;
3131
import java.util.List;
32+
import java.util.Map;
3233

3334
@RequiredArgsConstructor
34-
@SuppressWarnings("PMD.TooManyMethods")
35+
@SuppressWarnings({
36+
"PMD.TooManyMethods",
37+
// complains on "Series id must be non null"
38+
"PMD.AvoidDuplicateLiterals"
39+
})
3540
public class CollectionServiceImpl implements CollectionService {
3641

3742
private final Logger log;
@@ -96,14 +101,22 @@ public void addToCollection(Integer userId, AddToCollectionDto dto) {
96101
@Override
97102
@Transactional
98103
@PreAuthorize(HasAuthority.UPDATE_COLLECTION)
99-
public void removeFromCollection(Integer userId, Integer seriesId) {
104+
public void removeFromCollection(Integer userId, Integer seriesId, Integer seriesInstanceId) {
100105
Validate.isTrue(userId != null, "User id must be non null");
101106
Validate.isTrue(seriesId != null, "Series id must be non null");
107+
Validate.isTrue(seriesInstanceId != null, "Series instance id must be non null");
102108

103-
collectionDao.removeSeriesFromUserCollection(userId, seriesId);
109+
collectionDao.removeSeriesFromUserCollection(userId, seriesInstanceId);
104110
collectionDao.markAsModified(userId, new Date());
105111

106-
log.info("Series #{} has been removed from collection", seriesId);
112+
// The method accepts seriesId only for logging it.
113+
// As seriesId is provided by user and we don't check whether it's related to
114+
// seriesInstanceId, we can't fully rely on that but for the logging purposes it's enough
115+
log.info(
116+
"Series #{}: instance #{} has been removed from collection",
117+
seriesId,
118+
seriesInstanceId
119+
);
107120
}
108121

109122
@Override
@@ -119,6 +132,17 @@ public boolean isSeriesInCollection(Integer userId, Integer seriesId) {
119132
return collectionDao.isSeriesInUserCollection(userId, seriesId);
120133
}
121134

135+
// @todo #1123 CollectionService.findSeriesInstances(): add unit tests
136+
@Override
137+
@Transactional(readOnly = true)
138+
@PreAuthorize(HasAuthority.UPDATE_COLLECTION)
139+
public Map<Integer, Integer> findSeriesInstances(Integer userId, Integer seriesId) {
140+
Validate.isTrue(userId != null, "User id must be non null");
141+
Validate.isTrue(seriesId != null, "Series id must be non null");
142+
143+
return collectionDao.findSeriesInstances(userId, seriesId);
144+
}
145+
122146
@Override
123147
@Transactional(readOnly = true)
124148
public long countCollectionsOfUsers() {

src/main/java/ru/mystamps/web/feature/collection/JdbcCollectionDao.java

+28-7
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
import org.slf4j.LoggerFactory;
2424
import org.springframework.beans.factory.annotation.Value;
2525
import org.springframework.dao.EmptyResultDataAccessException;
26+
import org.springframework.jdbc.core.ResultSetExtractor;
2627
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
2728
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
2829
import org.springframework.jdbc.support.GeneratedKeyHolder;
2930
import org.springframework.jdbc.support.KeyHolder;
3031
import ru.mystamps.web.common.JdbcUtils;
3132
import ru.mystamps.web.common.LinkEntityDto;
33+
import ru.mystamps.web.support.spring.jdbc.MapIntegerIntegerResultSetExtractor;
3234

3335
import java.util.Collections;
3436
import java.util.Date;
@@ -41,6 +43,9 @@
4143
public class JdbcCollectionDao implements CollectionDao {
4244
private static final Logger LOG = LoggerFactory.getLogger(JdbcCollectionDao.class);
4345

46+
private static final ResultSetExtractor<Map<Integer, Integer>> INSTANCES_COUNTER_EXTRACTOR =
47+
new MapIntegerIntegerResultSetExtractor("id", "number_of_stamps");
48+
4449
private final NamedParameterJdbcTemplate jdbcTemplate;
4550

4651
@Value("${collection.find_last_created}")
@@ -73,11 +78,14 @@ public class JdbcCollectionDao implements CollectionDao {
7378
@Value("${collection.is_series_in_collection}")
7479
private String isSeriesInUserCollectionSql;
7580

81+
@Value("${collection.find_series_instances}")
82+
private String findSeriesInstancesSql;
83+
7684
@Value("${collection.add_series_to_collection}")
7785
private String addSeriesToCollectionSql;
7886

79-
@Value("${collection.remove_series_from_collection}")
80-
private String removeSeriesFromCollectionSql;
87+
@Value("${collection.remove_series_instance_from_collection}")
88+
private String removeSeriesInstanceSql;
8189

8290
@Value("${collection.find_info_by_slug}")
8391
private String findCollectionInfoBySlugSql;
@@ -218,6 +226,19 @@ public boolean isSeriesInUserCollection(Integer userId, Integer seriesId) {
218226
return result > 0;
219227
}
220228

229+
@Override
230+
public Map<Integer, Integer> findSeriesInstances(Integer userId, Integer seriesId) {
231+
Map<String, Object> params = new HashMap<>();
232+
params.put("user_id", userId);
233+
params.put("series_id", seriesId);
234+
235+
return jdbcTemplate.query(
236+
findSeriesInstancesSql,
237+
params,
238+
INSTANCES_COUNTER_EXTRACTOR
239+
);
240+
}
241+
221242
@Override
222243
public Integer addSeriesToUserCollection(AddToCollectionDbDto dto) {
223244
Map<String, Object> params = new HashMap<>();
@@ -250,17 +271,17 @@ public Integer addSeriesToUserCollection(AddToCollectionDbDto dto) {
250271

251272
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
252273
@Override
253-
public void removeSeriesFromUserCollection(Integer userId, Integer seriesId) {
274+
public void removeSeriesFromUserCollection(Integer userId, Integer seriesInstanceId) {
254275
Map<String, Object> params = new HashMap<>();
276+
params.put("id", seriesInstanceId);
255277
params.put("user_id", userId);
256-
params.put("series_id", seriesId);
257278

258-
int affected = jdbcTemplate.update(removeSeriesFromCollectionSql, params);
279+
int affected = jdbcTemplate.update(removeSeriesInstanceSql, params);
259280
if (affected != 1) {
260281
// CheckStyle: ignore LineLength for next 2 lines
261282
LOG.warn(
262-
"Unexpected number of affected rows after removing series #{} from collection of user #{}: {}",
263-
seriesId,
283+
"Unexpected number of affected rows after removing series instance #{} from collection of user #{}: {}",
284+
seriesInstanceId,
264285
userId,
265286
affected
266287
);

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

+14-5
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ public String addToCollection(
412412

413413
redirectAttributes.addFlashAttribute("justAddedSeries", true);
414414
redirectAttributes.addFlashAttribute("justAddedSeriesId", seriesId);
415+
redirectAttributes.addFlashAttribute("justAddedNumberOfStamps", form.getNumberOfStamps());
415416

416417
String collectionSlug = currentUserDetails.getUserCollectionSlug();
417418
return redirectTo(CollectionUrl.INFO_COLLECTION_PAGE, collectionSlug);
@@ -420,6 +421,7 @@ public String addToCollection(
420421
@PostMapping(path = SeriesUrl.INFO_SERIES_PAGE, params = "action=REMOVE")
421422
public String removeFromCollection(
422423
@PathVariable("id") Integer seriesId,
424+
@RequestParam(name = "id", defaultValue = "0") Integer seriesInstanceId,
423425
@AuthenticationPrincipal CustomUserDetails currentUserDetails,
424426
RedirectAttributes redirectAttributes,
425427
HttpServletResponse response)
@@ -430,14 +432,19 @@ public String removeFromCollection(
430432
return null;
431433
}
432434

435+
if (seriesInstanceId == 0) {
436+
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
437+
return null;
438+
}
439+
433440
boolean seriesExists = seriesService.isSeriesExist(seriesId);
434441
if (!seriesExists) {
435442
response.sendError(HttpServletResponse.SC_NOT_FOUND);
436443
return null;
437444
}
438445

439446
Integer userId = currentUserDetails.getUserId();
440-
collectionService.removeFromCollection(userId, seriesId);
447+
collectionService.removeFromCollection(userId, seriesId, seriesInstanceId);
441448

442449
redirectAttributes.addFlashAttribute("justRemovedSeries", true);
443450

@@ -644,10 +651,12 @@ public static void loadErrorsFromDownloadInterceptor(
644651

645652
boolean userCanAddImagesToSeries = isUserCanAddImagesToSeries(series);
646653
model.put("allowAddingImages", userCanAddImagesToSeries);
647-
648-
boolean isSeriesInCollection =
649-
collectionService.isSeriesInCollection(currentUserId, seriesId);
650-
model.put("isSeriesInCollection", isSeriesInCollection);
654+
655+
if (SecurityContextUtils.hasAuthority(Authority.UPDATE_COLLECTION)) {
656+
Map<Integer, Integer> seriesInstances =
657+
collectionService.findSeriesInstances(currentUserId, seriesId);
658+
model.put("seriesInstances", seriesInstances);
659+
}
651660

652661
if (SecurityContextUtils.hasAuthority(Authority.VIEW_SERIES_SALES)) {
653662
List<PurchaseAndSaleDto> purchasesAndSales =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (C) 2009-2019 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.support.spring.jdbc;
19+
20+
import lombok.RequiredArgsConstructor;
21+
import org.springframework.dao.DataAccessException;
22+
import org.springframework.jdbc.core.ResultSetExtractor;
23+
import ru.mystamps.web.common.JdbcUtils;
24+
25+
import java.sql.ResultSet;
26+
import java.sql.SQLException;
27+
import java.util.HashMap;
28+
import java.util.Map;
29+
30+
@RequiredArgsConstructor
31+
public class MapIntegerIntegerResultSetExtractor
32+
implements ResultSetExtractor<Map<Integer, Integer>> {
33+
34+
private final String keyFieldName;
35+
private final String valueFieldName;
36+
37+
@Override
38+
public Map<Integer, Integer> extractData(ResultSet rs)
39+
throws SQLException, DataAccessException {
40+
41+
Map<Integer, Integer> result = new HashMap<>();
42+
43+
while (rs.next()) {
44+
Integer key = JdbcUtils.getInteger(rs, keyFieldName);
45+
Integer value = JdbcUtils.getInteger(rs, valueFieldName);
46+
result.put(key, value);
47+
}
48+
49+
return result;
50+
}
51+
52+
}

src/main/resources/liquibase/version/0.4.2/2019-11-27--add_collections_series_id.xml

+10
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,14 @@
4141

4242
</changeSet>
4343

44+
<changeSet id="add-series-3-to-collection-once-more" author="php-coder" context="test-data">
45+
46+
<insert tableName="collections_series">
47+
<column name="collection_id" valueComputed="(SELECT id FROM collections WHERE slug = 'seriesowner')" />
48+
<column name="series_id" valueComputed="(SELECT id FROM series WHERE quantity = 3 ORDER BY id LIMIT 1)" />
49+
<column name="number_of_stamps" valueNumeric="2" />
50+
</insert>
51+
52+
</changeSet>
53+
4454
</databaseChangeLog>

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,12 @@ t_pick_percent_name = Pick "%name%"
132132
t_series_info = Info about series
133133
t_yes = Yes
134134
t_no = No
135-
t_series_not_in_collection = Series isn't part of your collection
135+
t_series_in_collection = You already have this series. Add another one instance
136+
t_series_not_in_collection = You don't have this series. Add one instance
136137
t_i_have = I have
137138
t_only_for_paid_users = Only for paid users
138139
t_i_bought_for = I bought for
139140
t_out_of_n_stamps = out of {0} stamps
140-
t_series_in_collection = Series is part of your collection
141141
t_add_to_collection = Add to collection
142142
t_remove_from_collection = Remove from collection
143143
t_need_authentication_to_add_series_to_collection = \

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,12 @@ t_pick_percent_name = Выбрать "%name%"
131131
t_series_info = Информация о серии
132132
t_yes = Да
133133
t_no = Нет
134-
t_series_not_in_collection = Этой серии нет в вашей коллекции
134+
t_series_in_collection = Эта серия уже есть в вашей коллекции. Добавить еще один экземпляр
135+
t_series_not_in_collection = Этой серии нет в вашей коллекции. Добавить экземпляр
135136
t_i_have = У меня есть
136137
t_only_for_paid_users = Только для платных пользователей
137138
t_i_bought_for = Я купил за
138139
t_out_of_n_stamps = из {0} марок
139-
t_series_in_collection = Эта серия есть в вашей коллекции
140140
t_add_to_collection = Добавить в коллекцию
141141
t_remove_from_collection = Убрать из коллекции
142142
t_need_authentication_to_add_series_to_collection = \

src/main/resources/sql/collection_dao_queries.properties

+10-2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ SELECT COUNT(*) \
100100
WHERE c.user_id = :user_id \
101101
AND cs.series_id = :series_id
102102

103+
collection.find_series_instances = \
104+
SELECT cs.id, cs.number_of_stamps \
105+
FROM collections c \
106+
JOIN collections_series cs \
107+
ON cs.collection_id = c.id \
108+
WHERE c.user_id = :user_id \
109+
AND cs.series_id = :series_id
110+
103111
collection.add_series_to_collection = \
104112
INSERT \
105113
INTO collections_series \
@@ -117,10 +125,10 @@ SELECT c.id AS collection_id \
117125
FROM collections c \
118126
WHERE c.user_id = :user_id
119127

120-
collection.remove_series_from_collection = \
128+
collection.remove_series_instance_from_collection = \
121129
DELETE \
122130
FROM collections_series \
123-
WHERE series_id = :series_id \
131+
WHERE id = :id \
124132
AND collection_id = ( \
125133
SELECT id \
126134
FROM collections \

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ <h4 class="panel-title" th:text="#{t_stamps_by_categories}">Stamps by categories
141141
th:if="${series.quantity != series.numberOfStamps}"
142142
th:text="#{t_m_out_of_n(${series.numberOfStamps}, ${series.quantity})}"></span>
143143
/*/-->
144-
<span th:if="${justAddedSeriesId != null and justAddedSeriesId == series.id}" class="label label-success">New</span>
144+
<span th:if="${justAddedSeriesId != null and justAddedSeriesId == series.id and justAddedNumberOfStamps == series.NumberOfStamps}" class="label label-success">New</span>
145+
<!--/* @todo #1123 /collection/{slug}: show "New" badge only once */-->
145146
</li>
146147
<li>
147148
<a href="../category/info.html">Animals, Italy, 22&nbsp;stamps</a>

0 commit comments

Comments
 (0)