Рубрики
Без рубрики

Третий раунд улучшений приложения Reddit

Улучшение приложения Reddit в тематическом исследовании с помощью: улучшенных таблиц, уведомлений по электронной почте, простых правил голосования и дополнительной проверки.

Автор оригинала: baeldung.

1. Обзор

В этой статье мы продолжим продвигать наш небольшой подход к тематическому исследованию, внедряя небольшие, но полезные улучшения в уже существующие функции.

2. Лучшие Таблицы

Давайте начнем с использования плагина jQuery DataTables для замены старых базовых таблиц, которые приложение использовало раньше.

2.1. Хранилище и Сервис сообщений

Во – первых, мы добавим метод для подсчета запланированных сообщений пользователя – конечно, используя синтаксис данных Spring:

public interface PostRepository extends JpaRepository {
    ...
    Long countByUser(User user);
}

Далее давайте быстро рассмотрим реализацию уровня сервиса – извлечение сообщений пользователя на основе параметров разбиения на страницы:

@Override
public List getPostsList(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 List constructDataAccordingToUserTimezone(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 List getScheduledPosts(
  @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 titleSubmission DateStatus Resubmit Attempts leftActions

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 handleInvalidResubmitOptions
  (RuntimeException ex, WebRequest request) {
    
    logger.error("400 Status Code", ex);
    String bodyOfResponse = ex.getLocalizedMessage();
    return new ResponseEntity(
      bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST);
}

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, войдя в приложение, выполнив работу и выйдя из него.

Он добирается туда.