Dritte Runde der Verbesserungen an der Reddit-Anwendung

Dritte Runde der Verbesserungen an der Reddit-Anwendung

1. Überblick

In diesem Artikel werden wirour little case study appweiter vorantreiben, indem wir kleine, aber nützliche Verbesserungen an den bereits vorhandenen Funktionen implementieren.

2. Bessere Tabellen

Beginnen wir mit der Verwendung des jQuery DataTables-Plugins, um die alten Basistabellen zu ersetzen, die die App zuvor verwendet hat.

2.1. Post Repository und Service

Zunächst fügen wircount the scheduled posts of a user eine Methode hinzu, wobei wir natürlich die Spring Data-Syntax nutzen:

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

Als nächstes werfen wir einen kurzen Blick aufthe service layer implementation - Abrufen der Beiträge eines Benutzers basierend auf Paginierungsparametern:

@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());
}

Wir sindconverting the dates based on the timezone of the user:

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. Die API mit Paginierung

Als Nächstes veröffentlichen wir diesen Vorgang mit vollständiger Paginierung und Sortierung über die 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);
}

Beachten Sie, wie wir einen benutzerdefinierten Header verwenden, um die Paginierungsinformationen an den Client zu übergeben. Es gibt andere, etwas mehr Standardways to do this - Möglichkeiten, die wir später untersuchen könnten.

Diese Implementierung ist jedoch einfach genug - wir haben eine einfache Methode zum Generieren von Paging-Informationen:

public PagingInfo generatePagingInfo(int page, int size) {
    long total = postRepository.countByUser(userService.getCurrentUser());
    return new PagingInfo(page, size, total);
}

Und dasPagingInfoelbst:

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. Vorderes Ende

Schließlich verwendet das einfache Front-End eine benutzerdefinierte JS-Methode, um mit der API zu interagieren und diejQuery DataTable parameters zu verarbeiten:

Post titleSubmission DateStatus Resubmit Attempts leftActions

2.4. API-Tests für Paging

Mit der jetzt veröffentlichten API können wira few simple API tests schreiben, um sicherzustellen, dass die Grundlagen des Paging-Mechanismus wie erwartet funktionieren:

@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. E-Mail Benachrichtigungen

Als Nächstes erstellen wir einen grundlegenden E-Mail-Benachrichtigungsfluss -where a user receives emails, wenn die geplanten Posts gesendet werden:

3.1. E-Mail-Konfiguration

Führen Sie zunächst die E-Mail-Konfiguration durch:

@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;
}

Zusammen mit den notwendigen Eigenschaften, um SMTP zum Laufen zu bringen:

smtp.host=email-smtp.us-east-1.amazonaws.com
smtp.port=465
smtp.protocol=smtps
smtp.username=example
smtp.password=
[email protected]

3.2. Feuern Sie ein Ereignis ab, wenn ein Beitrag veröffentlicht wird

Stellen wir jetzt sicher, dass wir ein Ereignis auslösen, wenn ein geplanter Beitrag erfolgreich in Reddit veröffentlicht wird:

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. Ereignis und Zuhörer

Die Event-Implementierung ist ziemlich unkompliziert:

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;
    }
}

Und der Zuhörer:

@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. Verwenden von Post Total Votes

Als nächstes werden wir einige Arbeiten durchführen, um die Optionen für die erneute Übermittlung aufit’s now working with the total number of votes zu vereinfachen, anstatt mit dem Upvote-Verhältnis (das schwer zu verstehen war) zu arbeiten.

Wir können die Gesamtzahl der Stimmen anhand des Post-Scores und des Upvote-Verhältnisses berechnen:

  • Score = Upvotes - Downvotes

  • Gesamtzahl der Stimmen = Upvotes + Downvotes

  • Upvote Ratio = Upvotes / Gesamtzahl der Stimmen

Und so:

Gesamtzahl der Stimmen = Math.round (Punktzahl / ((2 * Upvote Ratio) - 1))

Zunächst ändern wir unsere Punktelogik, um diese Gesamtzahl der Stimmen zu berechnen und zu verfolgen:

public PostScores getPostScores(Post post) {
    ...

    float ratio = node.get("upvote_ratio").floatValue();
    postScore.setTotalVotes(Math.round(postScore.getScore() / ((2 * ratio) - 1)));

    ...
}

Und natürlich werden wir davon Gebrauch machen, wennchecking if a post is considered failed or not:

private boolean didPostGoalFail(Post post) {
    PostScores postScores = getPostScores(post);
    int totalVotes = postScores.getTotalVotes();
    ...
    return (((score < post.getMinScoreRequired()) ||
            (totalVotes < post.getMinTotalVotes())) &&
            !((noOfComments > 0) && post.isKeepIfHasComments()));
}

Schließlich werden wir natürlich die altenratio-Felder aus der Verwendung entfernen.

5. Überprüfen Sie die Optionen für die erneute Übermittlung

Abschließend werden wir dem Benutzer helfen, indem wir den komplexen Wiedervorlageoptionen einige Überprüfungen hinzufügen:

5.1. ScheduledPost Service

Hier ist die einfache Methode voncheckIfValidResubmitOptions():

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;
}

Wir werden diese Validierung gut nutzen, wenn wir einen neuen Beitrag planen:

public Post schedulePost(boolean isSuperUser, Post post, boolean resubmitOptionsActivated)
  throws ParseException {
    if (resubmitOptionsActivated && !checkIfValidResubmitOptions(post)) {
        throw new InvalidResubmitOptionsException("Invalid Resubmit Options");
    }
    ...
}

Beachten Sie, dass bei eingeschalteter Logik für die erneute Übermittlung die folgenden Felder Werte ungleich Null aufweisen müssen:

  • Anzahl der Versuche

  • Zeitintervall

  • Mindestpunktzahl erforderlich

5.2. Ausnahmebehandlung

Schließlich - im Falle einer ungültigen Eingabe wird dasInvalidResubmitOptionsException in unserer Hauptfehlerbehandlungslogik behandelt:

@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. Testen Sie die Optionen zum erneuten Senden

Lassen Sie uns abschließend unsere Optionen für die erneute Übermittlung testen. Wir werden sowohl die Aktivierungs- als auch die Deaktivierungsbedingungen testen:

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. Fazit

In dieser Ausgabe haben wir einige Verbesserungen vorgenommen, diemoving the case study app in the right direction - Benutzerfreundlichkeit sind.

Die gesamte Idee der Reddit Scheduler-App besteht darin, dem Benutzer zu ermöglichen, schnell neue Artikel für Reddit zu planen, indem er in die App einsteigt, die Arbeit erledigt und aussteigt.

Es kommt dorthin.