Préserver l’histoire de Reddit Post Submissions

Préserver l'histoire de Reddit Post Submissions

1. Vue d'ensemble

Dans cette tranche dethe Reddit App case study, nous allons commencer à suivre lesthe history of submission attempts for a post et rendre les statuts plus descriptifs et faciles à comprendre.

2. Amélioration de l'entitéPost

Tout d'abord, commençons par remplacer l'ancien statut String dans l'entitéPost par une liste beaucoup plus complète de réponses de soumission, en gardant une trace de beaucoup plus d'informations:

public class Post {
    ...
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "post")
    private List submissionsResponse;
}

Voyons ensuite ce que nous suivons réellement dans cette nouvelle entité de réponse d'envoi:

@Entity
public class SubmissionResponse implements IEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private int attemptNumber;

    private String content;

    private Date submissionDate;

    private Date scoreCheckDate;

    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "post_id", nullable = false)
    private Post post;

    public SubmissionResponse(int attemptNumber, String content, Post post) {
        super();
        this.attemptNumber = attemptNumber;
        this.content = content;
        this.submissionDate = new Date();
        this.post = post;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Attempt No ").append(attemptNumber).append(" : ").append(content);
        return builder.toString();
    }
}

Notez que chaqueconsumed submission attempt a unSubmissionResponse, de sorte que:

  • attemptNumber: le numéro de cette tentative

  • content: la réponse détaillée de cette tentative

  • submissionDate: la date de soumission de cette tentative

  • scoreCheckDate: la date à laquelle nous avons vérifié le score desPost Reddit dans cette tentative

Et voici le référentiel Spring Data JPA simple:

public interface SubmissionResponseRepository extends JpaRepository {

    SubmissionResponse findOneByPostAndAttemptNumber(Post post, int attemptNumber);
}

3. Service de planification

Nous devons maintenant commencer à modifier la couche de service pour garder une trace de ces informations supplémentaires.

Nous allons d'abord nous assurer que nous avons des raisons de réussite ou d'échec bien formatées pour lesquelles la publication a été considérée comme un succès ou un échec:

private final static String SCORE_TEMPLATE = "score %d %s minimum score %d";
private final static String TOTAL_VOTES_TEMPLATE = "total votes %d %s minimum total votes %d";

protected String getFailReason(Post post, PostScores postScores) {
    StringBuilder builder = new StringBuilder();
    builder.append("Failed because ");
    builder.append(String.format(
      SCORE_TEMPLATE, postScores.getScore(), "<", post.getMinScoreRequired()));

    if (post.getMinTotalVotes() > 0) {
        builder.append(" and ");
        builder.append(String.format(TOTAL_VOTES_TEMPLATE,
          postScores.getTotalVotes(), "<", post.getMinTotalVotes()));
    }
    if (post.isKeepIfHasComments()) {
        builder.append(" and has no comments");
    }
    return builder.toString();
}

protected String getSuccessReason(Post post, PostScores postScores) {
    StringBuilder builder = new StringBuilder();
    if (postScores.getScore() >= post.getMinScoreRequired()) {
        builder.append("Succeed because ");
        builder.append(String.format(SCORE_TEMPLATE,
          postScores.getScore(), ">=", post.getMinScoreRequired()));
        return builder.toString();
    }
    if (
      (post.getMinTotalVotes() > 0) &&
      (postScores.getTotalVotes() >= post.getMinTotalVotes())
    ) {
        builder.append("Succeed because ");
        builder.append(String.format(TOTAL_VOTES_TEMPLATE,
          postScores.getTotalVotes(), ">=", post.getMinTotalVotes()));
        return builder.toString();
    }
    return "Succeed because has comments";
}

Maintenant, nous allons améliorer l'ancienne logique et leskeep track of this extra historical information:

private void submitPost(...) {
    ...
    if (errorNode == null) {
        post.setSubmissionsResponse(addAttemptResponse(post, "Submitted to Reddit"));
        ...
    } else {
        post.setSubmissionsResponse(addAttemptResponse(post, errorNode.toString()));
        ...
    }
}
private void checkAndReSubmit(Post post) {
    if (didIntervalPass(...)) {
        PostScores postScores = getPostScores(post);
        if (didPostGoalFail(post, postScores)) {
            ...
            resetPost(post, getFailReason(post, postScores));
        } else {
            ...
            updateLastAttemptResponse(
              post, "Post reached target score successfully " +
                getSuccessReason(post, postScores));
        }
    }
}
private void checkAndDeleteInternal(Post post) {
    if (didIntervalPass(...)) {
        PostScores postScores = getPostScores(post);
        if (didPostGoalFail(post, postScores)) {
            updateLastAttemptResponse(post,
              "Deleted from reddit, consumed all attempts without reaching score " +
                getFailReason(post, postScores));
            ...
        } else {
            updateLastAttemptResponse(post,
              "Post reached target score successfully " +
                getSuccessReason(post, postScores));
            ...
        }
    }
}
private void resetPost(Post post, String failReason) {
    ...
    updateLastAttemptResponse(post, "Deleted from Reddit, to be resubmitted " + failReason);
    ...
}

Notez ce que font en réalité les méthodes de niveau inférieur:

  • addAttemptResponse(): crée un nouvel enregistrementSubmissionResponse et l'ajoute à la publication (appelée à chaque tentative de soumission)

  • updateLastAttemptResponse(): mettre à jour la réponse de la dernière tentative (appelée lors de la vérification du score de l'article)

4. Post programmé DTO

Ensuite, nous modifierons le DTO pour nous assurer que ces nouvelles informations sont à nouveau exposées au client:

public class ScheduledPostDto {
    ...

    private String status;

    private List detailedStatus;
}

Et voici les simplesSubmissionResponseDto:

public class SubmissionResponseDto {

    private int attemptNumber;

    private String content;

    private String localSubmissionDate;

    private String localScoreCheckDate;
}

Nous modifierons également la méthode de conversion dans nosScheduledPostRestController:

private ScheduledPostDto convertToDto(Post post) {
    ...
    List response = post.getSubmissionsResponse();
    if ((response != null) && (response.size() > 0)) {
        postDto.setStatus(response.get(response.size() - 1).toString().substring(0, 30));
        List responsedto =
          post.getSubmissionsResponse().stream().
            map(res -> generateResponseDto(res)).collect(Collectors.toList());
        postDto.setDetailedStatus(responsedto);
    } else {
        postDto.setStatus("Not sent yet");
        postDto.setDetailedStatus(Collections.emptyList());
    }
    return postDto;
}

private SubmissionResponseDto generateResponseDto(SubmissionResponse responseEntity) {
    SubmissionResponseDto dto = modelMapper.map(responseEntity, SubmissionResponseDto.class);
    String timezone = userService.getCurrentUser().getPreference().getTimezone();
    dto.setLocalSubmissionDate(responseEntity.getSubmissionDate(), timezone);
    if (responseEntity.getScoreCheckDate() != null) {
        dto.setLocalScoreCheckDate(responseEntity.getScoreCheckDate(), timezone);
    }
    return dto;
}

5. L'extrémité avant

Ensuite, nous modifierons nosscheduledPosts.jsp front-end pour gérer notre nouvelle réponse:



6. Des tests

Enfin, nous allons effectuer un test unitaire simple sur nos nouvelles méthodes:

Tout d'abord, nous allons tester l'implémentation degetSuccessReason():

@Test
public void whenHasEnoughScore_thenSucceed() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    PostScores postScores = new PostScores(6, 10, 1);

    assertTrue(getSuccessReason(post, postScores).contains("Succeed because score"));
}

@Test
public void whenHasEnoughTotalVotes_thenSucceed() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    post.setMinTotalVotes(8);
    PostScores postScores = new PostScores(2, 10, 1);

    assertTrue(getSuccessReason(post, postScores).contains("Succeed because total votes"));
}

@Test
public void givenKeepPostIfHasComments_whenHasComments_thenSucceed() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    post.setKeepIfHasComments(true);
    final PostScores postScores = new PostScores(2, 10, 1);

    assertTrue(getSuccessReason(post, postScores).contains("Succeed because has comments"));
}

Ensuite, nous allons tester l'implémentation degetFailReason():

@Test
public void whenNotEnoughScore_thenFail() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    PostScores postScores = new PostScores(2, 10, 1);

    assertTrue(getFailReason(post, postScores).contains("Failed because score"));
}

@Test
public void whenNotEnoughTotalVotes_thenFail() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    post.setMinTotalVotes(15);
    PostScores postScores = new PostScores(2, 10, 1);

    String reason = getFailReason(post, postScores);
    assertTrue(reason.contains("Failed because score"));
    assertTrue(reason.contains("and total votes"));
}

@Test
public void givenKeepPostIfHasComments_whenNotHasComments_thenFail() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    post.setKeepIfHasComments(true);
    final PostScores postScores = new PostScores(2, 10, 0);

    String reason = getFailReason(post, postScores);
    assertTrue(reason.contains("Failed because score"));
    assertTrue(reason.contains("and has no comments"));
}

7. Conclusion

Dans cet article, nous avons introduit une visibilité très utile sur le cycle de vie d'un article Reddit. Nous pouvons maintenant voir exactement quand un message a été soumis et supprimé à chaque fois, ainsi que la raison exacte de chaque opération.