แก้ไขข่าว

8

แปลไปแล้ว

ในบทนี้ คุณจะได้

  • เพิ่มฟอร์มแก้ไขข่าว
  • กำหนดสิทธิการแก้ไข
  • จำกัดฟิลด์ที่แก้ไขได้
  • ตอนนี้เราก็สร้างข่าวใหม่ได้แล้ว ขั้นตอนต่อไปคือ ทำให้สามารถแก้ไขและลบข่าวได้ เนื่องจากในส่วนของหน้าจอนั้นค่อนข้างง่าย บทนี้เราก็จะอธิบายเพิ่มเติมเรื่องการจัดการสิทธิการใช้งานต่างๆของผู้ใช้ด้วย Meteor

    เริ่มแรกก็ผูกข้อมูลที่ตัวจัดการเส้นทางก่อน โดยเราจะเพิ่มเส้นทางไปที่หน้าแก้ไขข่าว และกำหนดชุดข้อมูลให้มัน

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() { return Meteor.subscribe('posts'); }
    });
    
    Router.route('/', {name: 'postsList'});
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/posts/:_id/edit', {
      name: 'postEdit',
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/submit', {name: 'postSubmit'});
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        if (Meteor.loggingIn()) {
          this.render(this.loadingTemplate);
        } else {
          this.render('accessDenied');
        }
      } else {
        this.next();
      }
    }
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    เทมเพลตหน้าแก้ไขข่าว

    ตอนนี้เราก็มาดูที่เทมเพลต ซึ่งเทมเพลต postEdit ของเราก็เป็นฟอร์มแบบที่ใช้กันทั่วไป

    <template name="postEdit">
      <form class="main form page">
        <div class="form-group">
          <label class="control-label" for="url">URL</label>
          <div class="controls">
              <input name="url" id="url" type="text" value="{{url}}" placeholder="Your URL" class="form-control"/>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="title">Title</label>
          <div class="controls">
              <input name="title" id="title" type="text" value="{{title}}" placeholder="Name your post" class="form-control"/>
          </div>
        </div>
        <input type="submit" value="Submit" class="btn btn-primary submit"/>
        <hr/>
        <a class="btn btn-danger delete" href="#">Delete post</a>
      </form>
    </template>
    
    client/templates/posts/post_edit.html

    แล้วก็ไฟล์ post_edit.js ที่ต้องใช้คู่กัน

    Template.postEdit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var currentPostId = this._id;
    
        var postProperties = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        }
    
        Posts.update(currentPostId, {$set: postProperties}, function(error) {
          if (error) {
            // display the error to the user
            alert(error.reason);
          } else {
            Router.go('postPage', {_id: currentPostId});
          }
        });
      },
    
      'click .delete': function(e) {
        e.preventDefault();
    
        if (confirm("Delete this post?")) {
          var currentPostId = this._id;
          Posts.remove(currentPostId);
          Router.go('postsList');
        }
      }
    });
    
    client/templates/posts/post_edit.js

    ถึงตรงนี้ คุณก็คงคุ้นเคยกับโค้ดเกือบทั้งหมดแล้ว

    เรามีฟังก์ชัน callback สองตัว ตัวแรกใช้กับเหตุการณ์ submit ของฟอร์ม และอีกตัวใช้เหตุการณ์ click ของลิงก์ delete

    ฟังก์ชัน callback ของการ delete นั้นง่ายมากๆ แค่ปิดการทำงานของการคลิ๊ก และยืนยันการทำงานกับผู้ใช้ ถ้าตอบตกลง ก็จะนำค่า ID ของข่าวปัจจุบันจากชุดข้อมูลของเทมเพลต มาใช้ลบข่าวนั้น และสุดท้ายก็ส่งผู้ใช้กลับไปที่หน้าโฮม

    ส่วน callback ของการอัพเดทนั้นยาวกว่านิดนึง แต่ก็ไม่ได้ซับซ้อนมากกว่าเดิมเท่าไหร่ หลังจากที่ปิดการทำงานเดิมของฟอร์ม และได้ค่า ID ของข่าวปัจจุบันมาแล้ว ก็จะดึงค่าฟิลด์จากหน้าเพจไปเก็บไว้ในอ็อบเจกต์ postProperties

    จากนั้นเราก็ส่งอ็อบเจกต์นี้ไปให้เมธอด Collection.update() ของ Meteor ด้วยตัวดำเนินการ $set (ที่จะเปลี่ยนเฉพาะค่าของฟิลด์ที่ระบุ โดยไม่ยุ่งกับฟิลด์อื่น) และใช้ callback เพื่อแสดงข้อความผิดพลาด หรือส่งผู้ใช้กลับไปที่หน้าข่าว ถ้าทำการอัพเดทได้สำเร็จ

    เพิ่มลิงก์การแก้ไข

    เพื่อให้ผู้ใช้รู้ว่าสามารถเข้าไปแก้ไขข่าวได้ เราก็ควรเพิ่มลิงก์แก้ไขเข้าไปในหน้าข่าวด้วย

    <template name="postItem">
      <div class="post">
        <div class="post-content">
          <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
          <p>
            submitted by {{author}}
            {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
          </p>
        </div>
        <a href="{{pathFor 'postPage'}}" class="discuss btn btn-default">Discuss</a>
      </div>
    </template>
    
    client/templates/posts/post_item.html

    และแน่นอนว่าเราไม่ต้องการให้คุณเห็นลิงก์แก้ไขที่หน้าข่าวของคนอื่น เราก็เลยต้องใช้ตัวช่วย ownPost กับงานนี้

    Template.postItem.helpers({
      ownPost: function() {
        return this.userId === Meteor.userId();
      },
      domain: function() {
        var a = document.createElement('a');
        a.href = this.url;
        return a.hostname;
      }
    });
    
    client/templates/posts/post_item.js
    Post edit form.
    Post edit form.

    หน้าฟอร์มแก้ไขข่าวของเราดูดีทีเดียว แต่ทว่าคุณก็ยังไม่สามารถแก้ไขอะไรได้จริงๆในตอนนี้ มันเกิดอะไรขึ้นกันแน่

    กำหนดสิทธิการใช้งาน

    ตั้งแต่ตอนที่เราถอนแพ็คเกจ insecure ออกไป การแก้ไขที่ไคลเอนต์ก็จะถูกปฏิเสธทั้งหมด

    วิธีแก้ไขเรื่องนี้ เราจะกำหนดสิทธิการใช้งานบางอย่างขึ้นมา โดยเริ่มจากสร้างไฟล์ permissions.js ใน lib เพื่อให้แน่ใจว่า โค้ดของการกำหนดสิทธิถูกโหลดไว้ตั้งแต่แรก (และใช้ได้กับทั้งสองฝั่ง)

    // check that the userId specified owns the documents
    ownsDocument = function(userId, doc) {
      return doc && doc.userId === userId;
    }
    
    lib/permissions.js

    ในบท สร้างข่าวใหม่ เราได้ลบเมธอด allow() ออกไป เพราะตอนนั้นเราใช้วิธีเพิ่มข่าวใหม่ด้วยเมธอดที่ฝั่งเซิร์ฟเวอร์ (ซึ่งไม่สนใจโค้ดของ allow() อยู่แล้ว)

    แต่ตอนนี้เรากำลังจะแก้ไขและลบข่าวด้วยโค้ดที่ฝั่งไคลเอนต์ ดังนั้นให้กลับไปที่ posts.js และเพิ่มบล็อก allow() เข้าไปใหม่

    Posts = new Mongo.Collection('posts');
    
    Posts.allow({
      update: function(userId, post) { return ownsDocument(userId, post); },
      remove: function(userId, post) { return ownsDocument(userId, post); },
    });
    
    //...
    
    lib/collections/posts.js

    คอมมิท 8-2

    Added basic permission to check the post’s owner.

    จำกัดการแก้ไข

    เพียงเพราะเราสามารถแก้ไขข่าวของตัวเองได้ ไม่ได้หมายความว่า เราต้องแก้ไข ทุกๆ ฟิลด์ ตัวอย่างเช่น เราไม่ต้องการให้ผู้ใช้สร้างข่าว แล้วส่งต่อมันให้เป็นของคนอื่น

    ดังนั้นเราจะใช้ฟังก์ชัน callback deny() ของ Meteor เพื่อจำกัดฟิลด์ให้เหลือเฉพาะเท่าที่ผู้ใช้สามารถแก้ไขได้

    Posts = new Mongo.Collection('posts');
    
    Posts.allow({
      update: function(userId, post) { return ownsDocument(userId, post); },
      remove: function(userId, post) { return ownsDocument(userId, post); },
    });
    
    Posts.deny({
      update: function(userId, post, fieldNames) {
        // may only edit the following two fields:
        return (_.without(fieldNames, 'url', 'title').length > 0);
      }
    });
    
    //...
    
    lib/collections/posts.js

    คอมมิท 8-3

    Only allow changing certain fields of posts.

    เรานำอาร์เรย์ fieldNames ที่ประกอบด้วยชื่อฟิลด์ซึ่งกำลังถูกแก้ไข มาใช้กับเมธอด without() ของ Underscore เพื่อหาค่าอาร์เรย์ของชื่อฟิลด์ที่ ไม่ใช่ url หรือ title

    ถ้าทุกอย่างเป็นปกติ อาร์เรย์ตัวนั้นควรจะว่างและขนาดของมันควรเป็น 0 แต่ถ้ามีใครบางคนลองอะไรแปลกๆ ขนาดของอาร์เรย์อาจจะเป็น 1 หรือมากกว่า และทำให้ callback คืนค่า true (เท่ากับปฏิเสธการอัพเดท)

    คุณอาจจะสังเกตุว่า ไม่มีตรงไหนในโค้ดของการแก้ไขข่าว ที่เราเช็คว่าลิงก์นั้นซ้ำหรือไม่ ซึ่งก็หมายความว่า ผู้ใช้สามารถป้อนลิงก์เข้ามาและทำการแก้ไข URL ของมันทีหลัง เพื่อเลี่ยงการตรวจสอบในตอนแรกได้ ซึ่งการแก้ไขในเรื่องนี้สามารถทำได้โดยใช้เมธอดของ Meteor กับหน้าฟอร์มแก้ไข แต่เราจะทิ้งเรื่องนี้ไว้เป็นแบบฝึกหัดให้ผู้อ่านแทน

    เปรียบเทียบระหว่างการใช้เมธอด กับ การจัดการข้อมูลที่ไคลเอนต์

    ในการสร้างข่าวนั้น เราเรียกใช้เมธอด postInsert ของ Meteor แต่ในขณะที่การแก้ไขและลบข่าว เราเรียกใช้ update และ remove โดยตรงที่ไคลเอนต์ โดยกำหนดสิทธิไว้ที่ allow และ deny

    แล้วเมื่อไรที่เราจะเลือกใช้อย่างนึง และไม่ใช้อีกอย่าง

    ถ้าสิ่งนั้นไม่ซับซ้อนอะไร และคุณสามารถกำหนดกฏเกณฑ์ง่ายๆที่จะ allow และ deny ได้ มันก็จะง่ายกว่าถ้าจะจัดการสิ่งนั้นที่ไคลเอนต์โดยตรง

    แต่เมื่อไรก็ตาม ที่คุณจำเป็นต้องจัดการบางสิ่งนอกเหนือการควบคุมของผู้ใช้ (เช่น บันทึกเวลาของข่าวใหม่ หรือกำหนดชื่อผู้ใช้ที่ถูกต้องให้กับข่าว) มันก็น่าจะดีกว่าถ้าจะใช้เมธอด

    การเรียกใช้เมธอด จะเหมาะสมกว่ากับสถานการณ์ต่อไปนี้

    • เมื่อคุณจำเป็นต้องรู้ค่า หรือต้องคืนค่าผ่านฟังก์ชัน callback มากกว่าที่จะคอยการทำงานแบบรีแอคทีฟและการซิงโครไนซ์ให้เกิดขึ้น
    • ในฟังก์ชันที่ใช้งานฐานข้อมูลมากๆ จะเป็นการสิ้นเปลืองถ้าจะส่งคอลเลคชั่นขนาดใหญ่ข้ามไปข้ามมา
    • ในการสรุปและรวบรวมข้อมูล (เช่น การนับ, ค่าเฉลี่ย, ผลรวม)

    ถ้าต้องการคำอธิบายที่ละเอียดกว่านี้ คุณสามารถเข้าไปดูเพิ่มเติมได้จากในบล็อกของเรา