Segunda Rodada de Melhorias no Aplicativo Reddit

Segunda Rodada de Melhorias no Aplicativo Reddit

1. Visão geral

Vamos continuar nossoongoing Reddit web app case study com uma nova rodada de melhorias, com o objetivo de tornar o aplicativo mais amigável e fácil de usar.

2. Paginação de postagens programadas

Primeiro - vamos listar as postagens programadaswith pagination, para tornar tudo mais fácil de ver e entender.

2.1. As Operações Paginadas

Usaremos Spring Data para gerar a operação de que precisamos, fazendo bom uso da interfacePageable para recuperar as postagens programadas do usuário:

public interface PostRepository extends JpaRepository {
    Page findByUser(User user, Pageable pageable);
}

E aqui está nosso método de controladorgetScheduledPosts():

private static final int PAGE_SIZE = 10;

@RequestMapping("/scheduledPosts")
@ResponseBody
public List getScheduledPosts(
  @RequestParam(value = "page", required = false) int page) {
    User user = getCurrentUser();
    Page posts =
      postReopsitory.findByUser(user, new PageRequest(page, PAGE_SIZE));

    return posts.getContent();
}

2.2. Exibir postagens paginadas

Agora - vamos implementar um controle de paginação simples no front end:

Post title

E aqui está como carregamos as páginas com jQuery simples:

$(function(){
    loadPage(0);
});

var currentPage = 0;
function loadNext(){
    loadPage(currentPage+1);
}

function loadPrev(){
    loadPage(currentPage-1);
}

function loadPage(page){
    currentPage = page;
    $('table').children().not(':first').remove();
    $.get("api/scheduledPosts?page="+page, function(data){
        $.each(data, function( index, post ) {
            $('.table').append(''+post.title+'');
        });
    });
}

À medida que avançamos, essa tabela manual será rapidamente substituída por um plug-in de tabela mais maduro, mas, por enquanto, isso funciona perfeitamente.

3. Mostrar a página de login para usuários não conectados

Quando um usuário acessa a raiz,they should get different pages if they’re logged in or not.

Se o usuário estiver logado, ele deverá ver sua página inicial / painel. Se eles não estiverem logados - eles devem ver a página de login:

@RequestMapping("/")
public String homePage() {
    if (SecurityContextHolder.getContext().getAuthentication() != null) {
        return "home";
    }
    return "index";
}

4. Opções avançadas para pós-reenvio

Remover e reenviar postagens no Reddit é uma funcionalidade útil e altamente eficaz. No entanto, queremos ter cuidado com isso e comhave full control quando devemos e quando não devemos fazer.

Por exemplo - talvez não desejemos remover uma postagem se ela já tiver comentários. No final do dia, comentários são engajamento e queremos respeitar a plataforma e as pessoas que comentam a postagem.

So – that’s the first small yet highly useful feature we’ll add - uma nova opção que nos permitirá remover uma postagem apenas se não houver comentários sobre ela.

Outra pergunta muito interessante para responder é - se a postagem for reenviada quantas vezes, mas ainda não obtiver a tração necessária - deixamos após a última tentativa ou não? Bem, como todas as perguntas interessantes, a resposta aqui é - "depende". Se for uma postagem normal, podemos simplesmente encerrar o dia e deixá-la no ar. No entanto, se for uma postagem superimportante e realmente quisermos ter certeza de que ela ganha alguma força, podemos excluí-la no final.

Portanto, este é o segundo recurso pequeno, mas muito útil, que construiremos aqui.

Finalmente - e as mensagens controversas? Uma publicação pode ter 2 votos no reddit porque tem votos positivos ou porque possui 100 votos positivos e 98 negativos. A primeira opção significa que não está ganhando força, enquanto a segunda significa que está ganhando muita força e que a votação é dividida.

So – this is the third small feature we’re going to add - uma nova opção para levar em conta a proporção de votos positivos e negativos ao determinar se precisamos remover a postagem ou não.

4.1. A EntidadePost

Primeiro, precisamos modificar nossa entidadePost:

@Entity
public class Post {
    ...
    private int minUpvoteRatio;
    private boolean keepIfHasComments;
    private boolean deleteAfterLastAttempt;
}

Aqui estão os 3 campos:

  • minUpvoteRatio: A proporção mínima de votos positivos que o usuário deseja que sua postagem alcance - a proporção de votos positivos representa a porcentagem do total de votos que são votos positivos [máximo = 100, mínimo = 0]

  • keepIfHasComments: Determine se o usuário deseja manter sua postagem se ela tiver comentários, apesar de não atingir a pontuação exigida.

  • deleteAfterLastAttempt: Determine se o usuário deseja excluir a postagem após o término da tentativa final sem atingir a pontuação exigida.

4.2. The Scheduler

Vamos agora integrar essas novas opções interessantes ao agendador:

@Scheduled(fixedRate = 3 * 60 * 1000)
public void checkAndDeleteAll() {
    List submitted =
      postReopsitory.findByRedditIDNotNullAndNoOfAttemptsAndDeleteAfterLastAttemptTrue(0);

    for (Post post : submitted) {
        checkAndDelete(post);
    }
}

Na parte mais interessante - a lógica real decheckAndDelete():

private void checkAndDelete(Post post) {
    if (didIntervalPass(post.getSubmissionDate(), post.getTimeInterval())) {
        if (didPostGoalFail(post)) {
            deletePost(post.getRedditID());
            post.setSubmissionResponse("Consumed Attempts without reaching score");
            post.setRedditID(null);
            postReopsitory.save(post);
        } else {
            post.setNoOfAttempts(0);
            post.setRedditID(null);
            postReopsitory.save(post);
        }
    }
}

E aqui está a implementação dedidPostGoalFail() -checking if the post failed to reach the predefined goal/score:

private boolean didPostGoalFail(Post post) {
    PostScores postScores = getPostScores(post);
    int score = postScores.getScore();
    int upvoteRatio = postScores.getUpvoteRatio();
    int noOfComments = postScores.getNoOfComments();
    return (((score < post.getMinScoreRequired()) ||
             (upvoteRatio < post.getMinUpvoteRatio())) &&
           !((noOfComments > 0) && post.isKeepIfHasComments()));
}

Também precisamos modificar a lógica que recupera as informações dePost do Reddit - para garantir que coletamos mais dados:

public PostScores getPostScores(Post post) {
    JsonNode node = restTemplate.getForObject(
      "http://www.reddit.com/r/" + post.getSubreddit() +
      "/comments/" + post.getRedditID() + ".json", JsonNode.class);
    PostScores postScores = new PostScores();

    node = node.get(0).get("data").get("children").get(0).get("data");
    postScores.setScore(node.get("score").asInt());

    double ratio = node.get("upvote_ratio").asDouble();
    postScores.setUpvoteRatio((int) (ratio * 100));

    postScores.setNoOfComments(node.get("num_comments").asInt());

    return postScores;
}

Estamos usando um objeto de valor simples para representar as pontuações conforme as extraímos da API do Reddit:

public class PostScores {
    private int score;
    private int upvoteRatio;
    private int noOfComments;
}

Finalmente, precisamos modificarcheckAndReSubmit() para definirredditID da postagem reenviada com sucesso paranull:

private void checkAndReSubmit(Post post) {
    if (didIntervalPass(post.getSubmissionDate(), post.getTimeInterval())) {
        if (didPostGoalFail(post)) {
            deletePost(post.getRedditID());
            resetPost(post);
        } else {
            post.setNoOfAttempts(0);
            post.setRedditID(null);
            postReopsitory.save(post);
        }
    }
}

Observe que:

  • checkAndDeleteAll(): executa a cada 3 minutos para ver se alguma postagem consumiu suas tentativas e pode ser excluída

  • getPostScores(): retornar \ {pontuação da postagem, proporção de votos positivos, número de comentários}

4.3. Modifique a página de programação

Precisamos adicionar as novas modificações ao nossoschedulePostForm.html:



5. Registros importantes por e-mail

A seguir, implementaremos uma configuração rápida, mas altamente útil em nossa configuração de logback -emailing of important logs (ERROR level). Obviamente, isso é bastante útil para rastrear erros com facilidade no início do ciclo de vida de um aplicativo.

Primeiro, vamos adicionar algumas dependências obrigatórias ao nossopom.xml:


    javax.activation
    activation
    1.1.1


    javax.mail
    mail
    1.4.1

Então, vamos adicionar umSMTPAppender ao nossologback.xml:



    
        
            ERROR
            ACCEPT
            DENY
        

        smtp.example.com
        [email protected]
        [email protected]
        [email protected]
        password
        %logger{20} - %m
        
    

    
        
        
    

E é isso - agora, o aplicativo implantado enviará por e-mail qualquer problema assim que acontecer.

6. Subreddits de cache

Acontece queauto-completing subreddits expensive. Sempre que um usuário começa a digitar um subreddit ao agendar uma postagem - precisamos acessar a API do Reddit para obter esses subreddits e mostrar ao usuário algumas sugestões. Não é ideal.

Em vez de chamar a API do Reddit - vamos simplesmente armazenar em cache os subreddits populares e usá-los para preenchimento automático.

6.1. Recuperar Subreddits

Primeiro, vamos recuperar os subreddits mais populares e salvá-los em um arquivo simples:

public void getAllSubreddits() {
    JsonNode node;
    String srAfter = "";
    FileWriter writer = null;
    try {
        writer = new FileWriter("src/main/resources/subreddits.csv");
        for (int i = 0; i < 20; i++) {
            node = restTemplate.getForObject(
              "http://www.reddit.com/" + "subreddits/popular.json?limit=100&after=" + srAfter,
              JsonNode.class);
            srAfter = node.get("data").get("after").asText();
            node = node.get("data").get("children");
            for (JsonNode child : node) {
                writer.append(child.get("data").get("display_name").asText() + ",");
            }
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                logger.error("Error while getting subreddits", e);
            }
        }
        writer.close();
    } catch (Exception e) {
        logger.error("Error while getting subreddits", e);
    }
}

Esta é uma implementação madura? No. Precisamos de mais alguma coisa? Não, nós não. Precisamos seguir em frente.

6.2. Subbreddit Autocomplete

A seguir, vamos ter certeza dethe subreddits are loaded into memory on application startup - fazendo com que o serviço implementeInitializingBean:

public void afterPropertiesSet() {
    loadSubreddits();
}
private void loadSubreddits() {
    subreddits = new ArrayList();
    try {
        Resource resource = new ClassPathResource("subreddits.csv");
        Scanner scanner = new Scanner(resource.getFile());
        scanner.useDelimiter(",");
        while (scanner.hasNext()) {
            subreddits.add(scanner.next());
        }
        scanner.close();
    } catch (IOException e) {
        logger.error("error while loading subreddits", e);
    }
}

Agora que os dados do subreddit estão todos carregados na memória,we can search over the subreddits without hitting the Reddit API:

public List searchSubreddit(String query) {
    return subreddits.stream().
      filter(sr -> sr.startsWith(query)).
      limit(9).
      collect(Collectors.toList());
}

É claro que a API que expõe as sugestões de subreddit permanece a mesma:

@RequestMapping(value = "/subredditAutoComplete")
@ResponseBody
public List subredditAutoComplete(@RequestParam("term") String term) {
    return service.searchSubreddit(term);
}

7. Métricas

Finalmente - vamos integrar algumas métricas simples ao aplicativo. Para obter mais informações sobre como criar esses tipos de métricas,I wrote about them in some detail here.

7.1. Filtro de servlet

Aqui, oMetricFilter simples:

@Component
public class MetricFilter implements Filter {

    @Autowired
    private IMetricService metricService;

    @Override
    public void doFilter(
      ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
        HttpServletRequest httpRequest = ((HttpServletRequest) request);
        String req = httpRequest.getMethod() + " " + httpRequest.getRequestURI();

        chain.doFilter(request, response);

        int status = ((HttpServletResponse) response).getStatus();
        metricService.increaseCount(req, status);
    }
}

Também precisamos adicioná-lo em nossoServletInitializer:

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    super.onStartup(servletContext);
    servletContext.addListener(new SessionListener());
    registerProxyFilter(servletContext, "oauth2ClientContextFilter");
    registerProxyFilter(servletContext, "springSecurityFilterChain");
    registerProxyFilter(servletContext, "metricFilter");
}

7.2. Serviço de métrica

E aqui está nossoMetricService:

public interface IMetricService {
    void increaseCount(String request, int status);

    Map getFullMetric();
    Map getStatusMetric();

    Object[][] getGraphData();
}

7.3. Controlador Métrico

E ela é a controladora básica responsável por expor essas métricas em HTTP:

@Controller
public class MetricController {

    @Autowired
    private IMetricService metricService;

    //

    @RequestMapping(value = "/metric", method = RequestMethod.GET)
    @ResponseBody
    public Map getMetric() {
        return metricService.getFullMetric();
    }

    @RequestMapping(value = "/status-metric", method = RequestMethod.GET)
    @ResponseBody
    public Map getStatusMetric() {
        return metricService.getStatusMetric();
    }

    @RequestMapping(value = "/metric-graph-data", method = RequestMethod.GET)
    @ResponseBody
    public Object[][] getMetricGraphData() {
        Object[][] result = metricService.getGraphData();
        for (int i = 1; i < result[0].length; i++) {
            result[0][i] = result[0][i].toString();
        }
        return result;
    }
}

8. Conclusão

Este estudo de caso está crescendo bem. Na verdade, o aplicativo começou como um tutorial simples sobre como fazer OAuth com a API Reddit; agora, está evoluindo para uma ferramenta útil para o usuário avançado do Reddit - especialmente em torno das opções de agendamento e reenvio.

Finalmente, como eu o tenho usado, parece que meus próprios envios para o Reddit geralmente estão ganhando muito mais força, então é sempre bom ver isso.