Preserve the History of Reddit Post Submissions – 保存Reddit帖子提交的历史

最后修改: 2015年 8月 26日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this installment of the Reddit App case study, we’re going to start keeping track of the history of submission attempts for a post, and make the statuses more descriptive and easy to understand.

在本期Reddit App 案例研究中,我们将开始跟踪帖子的提交尝试历史,并使状态更具描述性和易于理解。

2. Improving the Post Entity

2.改进Post实体

First, let’s start by replacing the old String status in the Post entity with a much more complete list of submission responses, keeping track of a lot more information:

首先,让我们先把Post实体中的旧的String status替换成一个更完整的提交响应列表,记录更多的信息。

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

Next, let’s see what we’re actually keeping track of in this new submission response entity:

接下来,让我们看看在这个新的提交响应实体中,我们实际上在跟踪什么。

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

Note that each consumed submission attempt has a SubmissionResponse, so that:

请注意,每个消耗的提交尝试都有一个SubmissionResponse,因此,。

  • attemptNumber: the number of this attempt
  • content: the detailed response of this attempt
  • submissionDate: the submission date of this attempt
  • scoreCheckDate: the date we checked the score of the Reddit Post in this attempt

And here is the simple Spring Data JPA repository:

这里是简单的Spring Data JPA资源库。

public interface SubmissionResponseRepository extends JpaRepository<SubmissionResponse, Long> {

    SubmissionResponse findOneByPostAndAttemptNumber(Post post, int attemptNumber);
}

3. Scheduling Service

3.日程安排服务

We now need to start modifying the service layer to keep track of this extra information.

我们现在需要开始修改服务层,以保持对这些额外信息的跟踪。

We’ll first make sure we have a nicely formatted success or failure reasons for why the Post was considered a success or a failure:

我们首先要确保我们有一个格式很好的成功或失败的原因,说明为什么这个帖子被认为是成功或失败的。

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

Now, we’ll improve the old logic and keep 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);
    ...
}

Note what the lower level methods are actually doing:

注意低级别的方法实际上在做什么。

  • addAttemptResponse(): creates a new SubmissionResponse record and adds it to the Post (called on every submission attempt)
  • updateLastAttemptResponse(): update the last attempt response (called while checking post’s score)

4. Scheduled Post DTO

4.预定后的DTO

Next, we’ll modify the DTO to make sure this new information gets exposed back to the client:

接下来,我们将修改DTO,以确保这些新信息被暴露给客户。

public class ScheduledPostDto {
    ...

    private String status;

    private List<SubmissionResponseDto> detailedStatus;
}

And here’s the simple SubmissionResponseDto:

而这里是简单的SubmissionResponseDto

public class SubmissionResponseDto {

    private int attemptNumber;

    private String content;

    private String localSubmissionDate;

    private String localScoreCheckDate;
}

We will also modify conversion method in our ScheduledPostRestController:

我们也将在我们的ScheduledPostRestController中修改转换方法。

private ScheduledPostDto convertToDto(Post post) {
    ...
    List<SubmissionResponse> response = post.getSubmissionsResponse();
    if ((response != null) && (response.size() > 0)) {
        postDto.setStatus(response.get(response.size() - 1).toString().substring(0, 30));
        List<SubmissionResponseDto> 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. Front End

5.前端

Next, we will modify our front-end scheduledPosts.jsp to handle our new response:

接下来,我们将修改我们的前端scheduledPosts.jsp以处理我们的新响应。

<div class="modal">
    <h4 class="modal-title">Detailed Status</h4>
    <table id="res"></table>
</div>

<script >
var loadedData = [];
var detailedResTable = $('#res').DataTable( {
    "searching":false,
    "paging": false,
    columns: [
        { title: "Attempt Number", data: "attemptNumber" },
        { title: "Detailed Status", data: "content" },
        { title: "Attempt Submitted At", data: "localSubmissionDate" },
        { title: "Attempt Score Checked At", data: "localScoreCheckDate" }
 ]
} );
           
$(document).ready(function() {
    $('#myposts').dataTable( {
        ...
        "columnDefs": [
            { "targets": 2, "data": "status",
              "render": function ( data, type, full, meta ) {
                  return data + 
                    ' <a href="#" onclick="showDetailedStatus('+meta.row+' )">More Details</a>';
              }
            },
            ....
        ],
        ...
    });
});

function showDetailedStatus(row){
    detailedResTable.clear().rows.add(loadedData[row].detailedStatus).draw();
    $('.modal').modal();
}

</script>

6. Tests

6.测试

Finally, we will perform a simple unit test on our new methods:

最后,我们将对我们的新方法进行一个简单的单元测试。

First, we’ll test the getSuccessReason() implementation:

首先,我们将测试getSuccessReason() 的实现。

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

Next, we will test the getFailReason() implementation:

接下来,我们将测试getFailReason()实现。

@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

7.结论

In this installment, we introduced some very useful visibility into the lifecycle of a Reddit post. We can now see exactly when a post was submitted and deleted each time, along with the exact reason for each operation.

在这一期中,我们对Reddit帖子的生命周期引入了一些非常有用的可见性。我们现在可以准确地看到一个帖子是什么时候提交的,以及每次删除的时间,还有每次操作的确切原因。