1. Обзор
В этой статье мы продолжим продвигать наш небольшой подход к тематическому исследованию, внедряя небольшие, но полезные улучшения в уже существующие функции.
2. Лучшие Таблицы
Давайте начнем с использования плагина jQuery DataTables для замены старых базовых таблиц, которые приложение использовало раньше.
2.1. Хранилище и Сервис сообщений
Во – первых, мы добавим метод для подсчета запланированных сообщений пользователя – конечно, используя синтаксис данных Spring:
public interface PostRepository extends JpaRepository{ ... Long countByUser(User user); }
Далее давайте быстро рассмотрим реализацию уровня сервиса – извлечение сообщений пользователя на основе параметров разбиения на страницы:
@Override public ListgetPostsList(int page, int size, String sortDir, String sort) { PageRequest pageReq = new PageRequest(page, size, Sort.Direction.fromString(sortDir), sort); Page posts = postRepository.findByUser(userService.getCurrentUser(), pageReq); return constructDataAccordingToUserTimezone(posts.getContent()); }
Мы преобразуем даты в зависимости от часового пояса пользователя :
private ListconstructDataAccordingToUserTimezone(List posts) { String timeZone = userService.getCurrentUser().getPreference().getTimezone(); return posts.stream().map(post -> new SimplePostDto( post, convertToUserTomeZone(post.getSubmissionDate(), timeZone))) .collect(Collectors.toList()); } private String convertToUserTomeZone(Date date, String timeZone) { dateFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); return dateFormat.format(date); }
2.2. API С Разбиением На Страницы
Далее мы опубликуем эту операцию с полной разбиением на страницы и сортировкой через API:
@RequestMapping(method = RequestMethod.GET) @ResponseBody public ListgetScheduledPosts( @RequestParam(value = "page", required = false, defaultValue = "0") int page, @RequestParam(value = "size", required = false, defaultValue = "10") int size, @RequestParam(value = "sortDir", required = false, defaultValue = "asc") String sortDir, @RequestParam(value = "sort", required = false, defaultValue = "title") String sort, HttpServletResponse response) { response.addHeader("PAGING_INFO", scheduledPostService.generatePagingInfo(page, size).toString()); return scheduledPostService.getPostsList(page, size, sortDir, sort); }
Обратите внимание, как мы используем пользовательский заголовок для передачи информации о разбиении на страницы клиенту. Есть и другие, несколько более стандартные способы сделать это – способы, которые мы могли бы изучить позже.
Однако эта реализация достаточно проста – у нас есть простой метод для генерации информации о подкачке:
public PagingInfo generatePagingInfo(int page, int size) { long total = postRepository.countByUser(userService.getCurrentUser()); return new PagingInfo(page, size, total); }
И сам PagingInfo :
public class PagingInfo { private long totalNoRecords; private int totalNoPages; private String uriToNextPage; private String uriToPrevPage; public PagingInfo(int page, int size, long totalNoRecords) { this.totalNoRecords = totalNoRecords; this.totalNoPages = Math.round(totalNoRecords / size); if (page > 0) { this.uriToPrevPage = "page=" + (page - 1) + "&size=" + size; } if (page < this.totalNoPages) { this.uriToNextPage = "page=" + (page + 1) + "&size=" + size; } } }
2.3. Передний Конец
Наконец, простой интерфейс будет использовать пользовательский метод JS для взаимодействия с API и обработки параметров jQuery DataTable :
Post title | Submission Date | Status | Resubmit Attempts left | Actions |
---|
2.4. Тестирование API для подкачки
Теперь, когда API опубликован, мы можем написать несколько простых тестов API , чтобы убедиться, что основы механизма подкачки работают должным образом:
@Test public void givenMoreThanOnePage_whenGettingUserScheduledPosts_thenNextPageExist() throws ParseException, IOException { createPost(); createPost(); createPost(); Response response = givenAuth(). params("page", 0, "size", 2).get(urlPrefix + "/api/scheduledPosts"); assertEquals(200, response.statusCode()); assertTrue(response.as(List.class).size() > 0); String pagingInfo = response.getHeader("PAGING_INFO"); long totalNoRecords = Long.parseLong(pagingInfo.split(",")[0].split("=")[1]); String uriToNextPage = pagingInfo.split(",")[2].replace("uriToNextPage=", "").trim(); assertTrue(totalNoRecords > 2); assertEquals(uriToNextPage, "page=1&size=2"); } @Test public void givenMoreThanOnePage_whenGettingUserScheduledPostsForSecondPage_thenCorrect() throws ParseException, IOException { createPost(); createPost(); createPost(); Response response = givenAuth(). params("page", 1, "size", 2).get(urlPrefix + "/api/scheduledPosts"); assertEquals(200, response.statusCode()); assertTrue(response.as(List.class).size() > 0); String pagingInfo = response.getHeader("PAGING_INFO"); long totalNoRecords = Long.parseLong(pagingInfo.split(",")[0].split("=")[1]); String uriToPrevPage = pagingInfo.split(",")[3].replace("uriToPrevPage=", "").trim(); assertTrue(totalNoRecords > 2); assertEquals(uriToPrevPage, "page=0&size=2"); }
3. Уведомления по электронной почте
Далее мы создадим базовый поток уведомлений по электронной почте – где пользователь получает электронные письма когда отправляются его запланированные сообщения:
3.1. Настройка электронной почты
Во-первых, давайте сделаем настройку электронной почты:
@Bean public JavaMailSenderImpl javaMailSenderImpl() { JavaMailSenderImpl mailSenderImpl = new JavaMailSenderImpl(); mailSenderImpl.setHost(env.getProperty("smtp.host")); mailSenderImpl.setPort(env.getProperty("smtp.port", Integer.class)); mailSenderImpl.setProtocol(env.getProperty("smtp.protocol")); mailSenderImpl.setUsername(env.getProperty("smtp.username")); mailSenderImpl.setPassword(env.getProperty("smtp.password")); Properties javaMailProps = new Properties(); javaMailProps.put("mail.smtp.auth", true); javaMailProps.put("mail.smtp.starttls.enable", true); mailSenderImpl.setJavaMailProperties(javaMailProps); return mailSenderImpl; }
Вместе с необходимыми свойствами, чтобы заставить SMTP работать:
smtp.host=email-smtp.us-east-1.amazonaws.com smtp.port=465 smtp.protocol=smtps smtp.username=example smtp.password= [email protected]
3.2. Запуск События При Публикации Сообщения
Теперь давайте убедимся, что мы запускаем событие, когда запланированный пост успешно публикуется в Reddit:
private void updatePostFromResponse(JsonNode node, Post post) { JsonNode errorNode = node.get("json").get("errors").get(0); if (errorNode == null) { ... String email = post.getUser().getPreference().getEmail(); eventPublisher.publishEvent(new OnPostSubmittedEvent(post, email)); } ... }
3.3. Событие и слушатель
Реализация события довольно проста:
public class OnPostSubmittedEvent extends ApplicationEvent { private Post post; private String email; public OnPostSubmittedEvent(Post post, String email) { super(post); this.post = post; this.email = email; } }
И слушатель:
@Component public class SubmissionListner implements ApplicationListener{ @Autowired private JavaMailSender mailSender; @Autowired private Environment env; @Override public void onApplicationEvent(OnPostSubmittedEvent event) { SimpleMailMessage email = constructEmailMessage(event); mailSender.send(email); } private SimpleMailMessage constructEmailMessage(OnPostSubmittedEvent event) { String recipientAddress = event.getEmail(); String subject = "Your scheduled post submitted"; SimpleMailMessage email = new SimpleMailMessage(); email.setTo(recipientAddress); email.setSubject(subject); email.setText(constructMailContent(event.getPost())); email.setFrom(env.getProperty("support.email")); return email; } private String constructMailContent(Post post) { return "Your post " + post.getTitle() + " is submitted.\n" + "http://www.reddit.com/r/" + post.getSubreddit() + "/comments/" + post.getRedditID(); } }
4. Использование Общего Количества Голосов После Публикации
Затем мы проделаем некоторую работу, чтобы упростить параметры повторной подачи – вместо того, чтобы работать с соотношением голосов (которое было трудно понять) – теперь он работает с общим количеством голосов .
Мы можем рассчитать общее количество голосов, используя оценку поста и соотношение голосов:
- Оценка – понижающие голоса
- Общее количество + нисходящих голосов
- Upvote/общее количество голосов
И вот:
Общее количество.раунд( оценка/((2 * соотношение голосов) – 1) )
Во-первых, мы изменим нашу логику оценки, чтобы рассчитать и отслеживать это общее количество голосов:
public PostScores getPostScores(Post post) { ... float ratio = node.get("upvote_ratio").floatValue(); postScore.setTotalVotes(Math.round(postScore.getScore() / ((2 * ratio) - 1))); ... }
И, конечно, мы собираемся использовать его, когда проверим, считается ли сообщение неудачным или нет :
private boolean didPostGoalFail(Post post) { PostScores postScores = getPostScores(post); int totalVotes = postScores.getTotalVotes(); ... return (((score < post.getMinScoreRequired()) || (totalVotes < post.getMinTotalVotes())) && !((noOfComments > 0) && post.isKeepIfHasComments())); }
Наконец, мы, конечно, удалим старые поля ratio из использования.
5. Проверьте Параметры Повторной Отправки
Наконец, мы поможем пользователю, добавив некоторые проверки в сложные параметры повторной отправки:
5.1. Регулярное Почтовое Обслуживание
Вот простой проверьте, действительны ли параметры повторной отправки() метод:
private boolean checkIfValidResubmitOptions(Post post) { if (checkIfAllNonZero( post.getNoOfAttempts(), post.getTimeInterval(), post.getMinScoreRequired())) { return true; } else { return false; } } private boolean checkIfAllNonZero(int... args) { for (int tmp : args) { if (tmp == 0) { return false; } } return true; }
Мы будем хорошо использовать эту проверку при планировании новой публикации:
public Post schedulePost(boolean isSuperUser, Post post, boolean resubmitOptionsActivated) throws ParseException { if (resubmitOptionsActivated && !checkIfValidResubmitOptions(post)) { throw new InvalidResubmitOptionsException("Invalid Resubmit Options"); } ... }
Обратите внимание, что если логика повторной отправки включена – следующие поля должны иметь ненулевые значения:
- Количество попыток
- Временной интервал
- Минимальный необходимый балл
5.2. Обработка исключений
Наконец, в случае недопустимого ввода исключение InvalidResubmitOptionsException обрабатывается в нашей основной логике обработки ошибок:
@ExceptionHandler({ InvalidResubmitOptionsException.class }) public ResponseEntity
5.3. Параметры Повторной Отправки Теста
Наконец, давайте теперь проверим наши параметры повторной отправки – мы проверим как условия активации, так и условия деактивации:
public class ResubmitOptionsLiveTest extends AbstractLiveTest { private static final String date = "2016-01-01 00:00"; @Test public void givenResubmitOptionsDeactivated_whenSchedulingANewPost_thenCreated() throws ParseException, IOException { Post post = createPost(); Response response = withRequestBody(givenAuth(), post) .queryParams("resubmitOptionsActivated", false) .post(urlPrefix + "/api/scheduledPosts"); assertEquals(201, response.statusCode()); Post result = objectMapper.reader().forType(Post.class).readValue(response.asString()); assertEquals(result.getUrl(), post.getUrl()); } @Test public void givenResubmitOptionsActivated_whenSchedulingANewPostWithZeroAttempts_thenInvalid() throws ParseException, IOException { Post post = createPost(); post.setNoOfAttempts(0); post.setMinScoreRequired(5); post.setTimeInterval(60); Response response = withRequestBody(givenAuth(), post) .queryParams("resubmitOptionsActivated", true) .post(urlPrefix + "/api/scheduledPosts"); assertEquals(400, response.statusCode()); assertTrue(response.asString().contains("Invalid Resubmit Options")); } @Test public void givenResubmitOptionsActivated_whenSchedulingANewPostWithZeroMinScore_thenInvalid() throws ParseException, IOException { Post post = createPost(); post.setMinScoreRequired(0); post.setNoOfAttempts(3); post.setTimeInterval(60); Response response = withRequestBody(givenAuth(), post) .queryParams"resubmitOptionsActivated", true) .post(urlPrefix + "/api/scheduledPosts"); assertEquals(400, response.statusCode()); assertTrue(response.asString().contains("Invalid Resubmit Options")); } @Test public void givenResubmitOptionsActivated_whenSchedulingANewPostWithZeroTimeInterval_thenInvalid() throws ParseException, IOException { Post post = createPost(); post.setTimeInterval(0); post.setMinScoreRequired(5); post.setNoOfAttempts(3); Response response = withRequestBody(givenAuth(), post) .queryParams("resubmitOptionsActivated", true) .post(urlPrefix + "/api/scheduledPosts"); assertEquals(400, response.statusCode()); assertTrue(response.asString().contains("Invalid Resubmit Options")); } @Test public void givenResubmitOptionsActivated_whenSchedulingNewPostWithValidResubmitOptions_thenCreated() throws ParseException, IOException { Post post = createPost(); post.setMinScoreRequired(5); post.setNoOfAttempts(3); post.setTimeInterval(60); Response response = withRequestBody(givenAuth(), post) .queryParams("resubmitOptionsActivated", true) .post(urlPrefix + "/api/scheduledPosts"); assertEquals(201, response.statusCode()); Post result = objectMapper.reader().forType(Post.class).readValue(response.asString()); assertEquals(result.getUrl(), post.getUrl()); } private Post createPost() throws ParseException { Post post = new Post(); post.setTitle(randomAlphabetic(6)); post.setUrl("test.com"); post.setSubreddit(randomAlphabetic(6)); post.setSubmissionDate(dateFormat.parse(date)); return post; } }
6. Заключение
В этом выпуске мы внесли несколько улучшений, которые двигают приложение case study в правильном направлении – простота использования.
Вся идея приложения Reddit Scheduler заключается в том, чтобы позволить пользователю быстро планировать новые статьи в Reddit, войдя в приложение, выполнив работу и выйдя из него.
Он добирается туда.