Voting

13

Tercüme Yüzdesi

Bu bölümde:

  • Build a system where users can vote on posts.
  • Rank our posts by vote on a "best" post page.
  • Learn how to write a general handlebars helper.
  • Learn a little more about data security in Meteor.
  • Cover some interesting performance considerations in MongoDB.
  • ////

    ////

    ////

    Data Model

    ////

    Data Privacy & Publications

    ////

    ////

    ////

    ////

    // Fixture data 
    if (Posts.find().count() === 0) {
      var now = new Date().getTime();
    
      // create two users
      var tomId = Meteor.users.insert({
        profile: { name: 'Tom Coleman' }
      });
      var tom = Meteor.users.findOne(tomId);
      var sachaId = Meteor.users.insert({
        profile: { name: 'Sacha Greif' }
      });
      var sacha = Meteor.users.findOne(sachaId);
    
      var telescopeId = Posts.insert({
        title: 'Introducing Telescope',
        userId: sacha._id,
        author: sacha.profile.name,
        url: 'http://sachagreif.com/introducing-telescope/',
        submitted: now - 7 * 3600 * 1000,
        commentsCount: 2,
        upvoters: [], votes: 0
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: tom._id,
        author: tom.profile.name,
        submitted: now - 5 * 3600 * 1000,
        body: 'Interesting project Sacha, can I get involved?'
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: sacha._id,
        author: sacha.profile.name,
        submitted: now - 3 * 3600 * 1000,
        body: 'You sure can Tom!'
      });
    
      Posts.insert({
        title: 'Meteor',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://meteor.com',
        submitted: now - 10 * 3600 * 1000,
        commentsCount: 0,
        upvoters: [], votes: 0
      });
    
      Posts.insert({
        title: 'The Meteor Book',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://themeteorbook.com',
        submitted: now - 12 * 3600 * 1000,
        commentsCount: 0,
        upvoters: [], votes: 0
      });
    
      for (var i = 0; i < 10; i++) {
        Posts.insert({
          title: 'Test post #' + i,
          author: sacha.profile.name,
          userId: sacha._id,
          url: 'http://google.com/?q=test-' + i,
          submitted: now - i * 3600 * 1000,
          commentsCount: 0,
          upvoters: [], votes: 0
        });
      }
    }
    
    server/fixtures.js

    ////

    //...
    
    // check that there are no previous posts with the same link
    if (postAttributes.url && postWithSameLink) {
      throw new Meteor.Error(302, 
        'This link has already been posted', 
        postWithSameLink._id);
    }
    
    // pick out the whitelisted keys
    var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
      userId: user._id, 
      author: user.username, 
      submitted: new Date().getTime(),
      commentsCount: 0,
      upvoters: [], 
      votes: 0
    });
    
    var postId = Posts.insert(post);
    
    return postId;
    
    //...
    
    collections/posts.js

    Building our Voting Templates

    ////

    <template name="postItem">
      <div class="post">
        <a href="#" class="upvote btn"></a>
        <div class="post-content">
          <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
          <p>
            {{votes}} Votes,
            submitted by {{author}},
            <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
            {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
          </p>
        </div>
        <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
      </div>
    </template>
    
    client/views/posts/post_item.html
    The upvote button
    The upvote button

    ////

    //...
    
    Template.postItem.events({
      'click .upvote': function(e) {
        e.preventDefault();
        Meteor.call('upvote', this._id);
      }
    });
    
    client/views/posts/post_item.js

    ////

    Meteor.methods({
      post: function(postAttributes) {
        //...
      },
    
      upvote: function(postId) {
        var user = Meteor.user();
        // ensure the user is logged in
        if (!user)
          throw new Meteor.Error(401, "You need to login to upvote");
    
        var post = Posts.findOne(postId);
        if (!post)
          throw new Meteor.Error(422, 'Post not found');
    
        if (_.include(post.upvoters, user._id))
          throw new Meteor.Error(422, 'Already upvoted this post');
    
        Posts.update(post._id, {
          $addToSet: {upvoters: user._id},
          $inc: {votes: 1}
        });
      }
    });
    
    collections/posts.js

    Commit 13-1

    Added basic upvoting algorithm.

    ////

    ////

    User Interface Tweaks

    ////

    <template name="postItem">
      <div class="post">
        <a href="#" class="upvote btn {{upvotedClass}}"></a>
        <div class="post-content">
          //...
      </div>
    </template>
    
    client/views/posts/post_item.html
    Template.postItem.helpers({
      ownPost: function() {
        //...
      },
      domain: function() {
        //...
      },
      upvotedClass: function() {
        var userId = Meteor.userId();
        if (userId && !_.include(this.upvoters, userId)) {
          return 'btn-primary upvotable';
        } else {
          return 'disabled';
        }
      }
    });
    
    Template.postItem.events({
      'click .upvotable': function(e) {
        e.preventDefault();
        Meteor.call('upvote', this._id);
      }
    });
    
    client/views/posts/post_item.js

    ////

    Greying out upvote buttons.
    Greying out upvote buttons.

    Commit 13-2

    Grey out upvote link when not logged in / already voted.

    ////

    Handlebars.registerHelper('pluralize', function(n, thing) {
      // fairly stupid pluralizer
      if (n === 1) {
        return '1 ' + thing;
      } else {
        return n + ' ' + thing + 's';
      }
    });
    
    client/helpers/handlebars.js

    ////

    <template name="postItem">
    //...
    <p>
      {{pluralize votes "Vote"}},
      submitted by {{author}},
      <a href="{{pathFor 'postPage'}}">{{pluralize commentsCount "comment"}}</a>
      {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
    </p>
    //...
    </template>
    
    client/views/posts/post_item.html
    Perfecting Proper Pluralization (now say that 10 times)
    Perfecting Proper Pluralization (now say that 10 times)

    Commit 13-3

    Added pluralize helper to format text better.

    ////

    Smarter Voting Algorithm

    ////

    ////

    1. ////
    2. ////
    3. ////

    ////

    Meteor.methods({
      post: function(postAttributes) {
        //...
      },
    
      upvote: function(postId) {
        var user = Meteor.user();
        // ensure the user is logged in
        if (!user)
          throw new Meteor.Error(401, "You need to login to upvote");
    
        Posts.update({
          _id: postId, 
          upvoters: {$ne: user._id}
        }, {
          $addToSet: {upvoters: user._id},
          $inc: {votes: 1}
        });
      }
    });
    
    collections/posts.js

    Commit 13-4

    Better upvoting algorithm.

    ////

    ////

    Latency Compensation

    ////

    > Posts.update(postId, {$set: {votes: 10000}});
    
    Browser console

    ////

    ////

    ////

    ////

    ////

    Ranking the Front Page Posts

    ////

    ////

    ////

    ////

    PostsListController = RouteController.extend({
      template: 'postsList',
      increment: 5, 
      limit: function() { 
        return parseInt(this.params.postsLimit) || this.increment; 
      },
      findOptions: function() {
        return {sort: this.sort, limit: this.limit()};
      },
      waitOn: function() {
        return Meteor.subscribe('posts', this.findOptions());
      },
      posts: function() {
        return Posts.find({}, this.findOptions());
      },
      data: function() {
        var hasMore = this.posts().fetch().length === this.limit();
        return {
          posts: this.posts(),
          nextPath: hasMore ? this.nextPath() : null
        };
      }
    });
    
    NewPostsListController = PostsListController.extend({
      sort: {submitted: -1, _id: -1},
      nextPath: function() {
        return Router.routes.newPosts.path({postsLimit: this.limit() + this.increment})
      }
    });
    
    BestPostsListController = PostsListController.extend({
      sort: {votes: -1, submitted: -1, _id: -1},
      nextPath: function() {
        return Router.routes.bestPosts.path({postsLimit: this.limit() + this.increment})
      }
    });
    
    Router.map(function() {
      this.route('home', {
        path: '/',
        controller: NewPostsListController
      });
    
      this.route('newPosts', {
        path: '/new/:postsLimit?',
        controller: NewPostsListController
      });
    
      this.route('bestPosts', {
        path: '/best/:postsLimit?',
        controller: BestPostsListController
      });
      // ..
    });
    
    lib/router.js

    ////

    ////

    ////

    <template name="header">
      <header class="navbar">
        <div class="navbar-inner">
          <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </a>
          <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
          <div class="nav-collapse collapse">
            <ul class="nav">
              <li>
                <a href="{{pathFor 'newPosts'}}">New</a>
              </li>
              <li>
                <a href="{{pathFor 'bestPosts'}}">Best</a>
              </li>
              {{#if currentUser}}
                <li>
                  <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
                </li>
                <li class="dropdown">
                  {{> notifications}}
                </li>
              {{/if}}
            </ul>
            <ul class="nav pull-right">
              <li>{{loginButtons}}</li>
            </ul>
          </div>
        </div>
      </header>
    </template>
    
    client/views/include/header.html

    ////

    Ranking by points
    Ranking by points

    Commit 13-5

    Added routes for post lists, and pages to display them.

    A Better Header

    ////

    ////

    <template name="header">
      <header class="navbar">
        <div class="navbar-inner">
          <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </a>
          <a class="brand" href="{{pathFor 'home'}}">Microscope</a>
          <div class="nav-collapse collapse">
            <ul class="nav">
              <li class="{{activeRouteClass 'home' 'newPosts'}}">
                <a href="{{pathFor 'newPosts'}}">New</a>
              </li>
              <li class="{{activeRouteClass 'bestPosts'}}">
                <a href="{{pathFor 'bestPosts'}}">Best</a>
              </li>
              {{#if currentUser}}
                <li class="{{activeRouteClass 'postSubmit'}}">
                  <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
                </li>
                <li class="dropdown">
                  {{> notifications}}
                </li>
              {{/if}}
            </ul>
            <ul class="nav pull-right">
              <li>{{loginButtons}}</li>
            </ul>
          </div>
        </div>
      </header>
    </template>
    
    client/views/includes/header.html
    Template.header.helpers({
      activeRouteClass: function(/* route names */) {
        var args = Array.prototype.slice.call(arguments, 0);
        args.pop();
    
        var active = _.any(args, function(name) {
          return Router.current().route.name === name
        });
    
        return active && 'active';
      }
    });
    
    client/views/includes/header.js
    Showing the active page
    Showing the active page

    Helper Arguments

    ////

    ////

    ////

    ////

    ////

    Commit 13-6

    Added active classes to the header.

    ////