Editando posts

8

Porcentaje completado

En este capítulo:

  • Añadiremos un formulario para editar posts.
  • Configuraremos los permisos de edición.
  • Restringiremos las propiedades que se pueden editar.
  • Ahora que ya podemos crear posts, el siguiente paso es poder editarlos y borrarlos. Como el código de la IU ha quedado bastante simple, este parece un buen momento para hablar de cómo se gestionan los permisos de usuario con Meteor.

    Primero vamos a configurar nuestro router. Añadiremos una ruta para acceder a la página de edición y estableceremos su contexto de datos:

    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

    La plantilla de edición de posts

    Ahora ya nos podemos centrar en la plantilla. Nuestra plantilla postEdit tendrá una forma bastante estándar:

    <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

    Y aquí tenemos el fichero post_edit.js que la acompaña:

    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

    Como podemos ver, la mayoría de cosas ya nos son familiares.

    Tenemos dos callbacks de eventos: uno para enviar el formulario submit, y click .delete para el evento click del enlace delete.

    El callback delete es muy simple: elimina el evento predeterminado y después pide confirmación. Si confirmamos, obtenemos el ID del post actual desde el contexto de datos de la plantilla, lo borramos y redirigimos al usuario a la página de inicio.

    El callback de actualización es un poco más largo, pero no mucho más complicado. Después de suprimir el evento predeterminado y conseguir el post actual, obtiene los nuevos valores del formulario y los almacena en el objeto postProperties.

    A continuación, pasa este objeto al método Meteor Collection.update() usando el operador $set (que reemplaza un conjunto de atributos dejando los demás intactos), y usa un callback para mostrar un error si falla la actualización o envía al usuario a la página del post si la actualización se realiza correctamente.

    Añadiendo enlaces

    Deberíamos añadir enlaces a la página de edición de nuestros posts para que los usuarios puedan llegar a ella:

    <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

    Por supuesto, no queremos que se muestre el enlace para editar un post que no haya sido creado por ese usuario. Aquí es donde entra el ayudante 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
    Formulario de edición.
    Formulario de edición.

    Commit 8-1

    Añadido el formulario de edición.

    Ya tenemos nuestro formulario de envío de edición, pero en realidad, todavía no se puede editar nada. ¿Qué es lo que está pasando?

    Configurando los permisos

    Al haber eliminado el paquete insecure, se nos deniegan todas las peticiones de modificación desde el cliente.

    Para solucionarlo, estableceremos algunas reglas de permisos. Primero, creamos un nuevo archivo permissions.js dentro de lib. Esto nos asegura que nuestra lógica de permisos se cargará lo primero (y estará disponible en los dos entornos):

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

    En el capítulo Creando posts nos libramos de tener que usar el método allow() porque estábamos insertando nuevos posts a través de un método de servidor.

    Pero ahora que estamos editando y borrando posts desde el cliente, vamos a necesitar volver a collections/posts.js y a añadir un bloque 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

    Commit 8-2

    Añadidos permisos básicos para comprobar el dueño del post.

    Limitando las ediciones

    Solo porque podamos editar nuestros propios posts, no significa debamos ser capaces de editar todas las propiedades. Por ejemplo, no queremos que los usuarios puedan crear un post y luego asignárselo a otro usuario.

    Utilizaremos el callback deny() para asegurarnos de que los usuarios solo puedan editar los atributos especificados:

    Posts = new Mongo.Collection('posts');
    
    Posts.allow({
      update: ownsDocument,
      remove: ownsDocument
    });
    
    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

    Commit 8-3

    Permitir cambios sólo en ciertos campos.

    Estamos cogiendo el array fieldNames que contiene la lista de los campos que quieren modificar, y usamos el método without() de Underscore para devolver un sub-array que contiene los campos que no son url o title.

    Si todo va bien, el array debe estar vacío y su longitud debe ser 0. Pero si alguien está tratando de enredar, la longitud del array será mayor que 0, y devolveremos true (denegando así la actualización).

    Te habrás dado cuenta de que, en el código de edición del post, no comprobamos si hay enlaces duplicados. Esto significa que un usuario podría enviar un enlace y después editarlo y cambiar su URL para saltarse la comprobación. La solución a este problema podría ser utilizar un método Meteor para tratar este formulario de edición, pero dejaremos esto como un ejercicio para el lector.

    Métodos en el servidor vs. métodos en el cliente

    Para crear un post, estamos utilizando un método postInsert en el servidor, mientras que para editarlos y eliminarlos, llamamos a update y remove directamente desde el cliente, limitando el acceso a través de allow y deny.

    ¿Cuándo es más adecuado usar uno u otro?

    Cuando las cosas son relativamente sencillas se pueden expresar las reglas a través de allow y deny, por lo general es más fácil de hacer las cosas directamente desde el cliente.

    Sin embargo, tan pronto como empecemos a hacer cosas que deberían estar fuera del control del usuario (por ejemplo, el timestamp de un nuevo post o asignarlo al usuario correcto), será mejor que usemos métodos.

    Las llamadas a métodos también son apropiadas en algunos otros casos:

    • Cuando necesitamos conocer o devolver valores a través de un callback en lugar de esperar a que Meteor propague la sincronización.
    • Para consultas pesadas a la base de datos.
    • Para resumir o agregar datos (por ejemplo, contadores, promedios, sumas).

    Para conocer más a fondo este tema echa un vistazo a nuestro blog.