Modificare i post

8

Percentuale Tradotta

In questo capitolo imparerai:

  • Aggiungere un form per modificare i messaggi.
  • Imposta la modifica dei permessi.
  • Limita quali proprietà possono essere modificate.
  • Il prossimo passo, ora che abbiamo creato dei messaggi, è essere in grado di modificarli e cancellarli. Mentre il codice per l'interfaccia utente (UI) è abbastanza semplice, questo sarebbe un momento buono per parlare di come Meteor gestisce i permessi utente.

    Prima di tutto agganciamo il nostro router. Aggiungeremo una route per accedere alla pagina per modificare i messaggi e impostare il contesto dei dati (data context)

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

    Il template per la modifica messaggio

    Ora possiamo concentrarci sul template. Il nostro template postEdit sarà un form abbastanza standard:

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

    E qui c'è il gestore post_edit.js che và assieme:

    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/views/posts/post_edit.js

    A questo punto la maggior parte di questo codice dovrebbe essere familiare. Prima di tutto, abbiamo il nostro template helper che prende il messaggio corrente e lo passa al template.

    Abbiamo inoltre due callbacks per gli eventi di template: una per l'evento submit del form e una per l'evento click del link per cancellare.

    La callback per cancellare è estremamente semplice: sopprimiamo l'evento click predefinito e chiediamo una conferma. Se la riceviamo, otteniamo l'ID del messaggio corrente dal contesto dei dati (data context) del template, lo cancelliamo, e dopodichè reindirizziamo l'utente alla pagina principale.

    La callback di aggiornamento è un pò più lunga, ma non troppo complicata. Dopo aver soppresso l'evento predefinito e aver preso il messaggio corrente, prendiamo i nuovi valori del form dalla pagina e li memorizziamo in un oggetto postProperties.

    A quel punto passiamo quest'oggetto al metodo Collection.update() di Meteor e usiamo una callback che o visualizza un errore nel caso l'aggiornamento fallisce, oppure manda indietro l'utente alla pagina del messaggio se l'aggiornamento riesce.

    Aggiungere i links

    Dovremmo anche aggiungere dei links di modifica ai nostri messaggi, così che gli utenti possano accedere alla pagina di modifica:

    <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">Discuss</a>
      </div>
    </template>
    
    client/views/posts/post_item.html

    Ovviamente non vogliamo mostrare un link di modifica nel form di qualcun'altro. È qui che l'helper ownPost entra in scena:

    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/views/posts/post_item.js
    Form di modifica messaggio.
    Form di modifica messaggio.

    Commit 8-1

    Added edit posts form.

    Il nostro form di modifica sembra buono, ma in realtà non si può modificare niente. Che sta succedendo?

    Impostare i permessi

    Siccome precedentemente abbiamo rimosso il pacchetto (package) insecure, tutte le modifiche fatte sulla parte del client vengono negate.

    Per risolvere questo problema, imposteremo alcune regole di permessi. Prima di tutto, crea un nuovo permissions.js file all'interno di lib. Questo carica la nostra logica dei permessi (ed è disponibile in entrambi gli ambienti):

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

    Nel capitolo Creare post abbiamo rimosso la funzione allow() in quanto le modifiche al database avvenivano solamente sul server tramite un metodo Meteor, e, in questi casi, le direttive .allow() e .deny() vengono ignorate.

    Ora che, invece, editiamo contenuti sul client è necessario specificare chi ha i permessi di modificare gli elementi della collezione inserendo il metodo .allow() nel file posts.js.

    Posts = new Meteor.Collection('posts');
    
    Posts.allow({
      update: ownsDocument,
      remove: ownsDocument
    });
    
    Meteor.methods({
      ...
    
    collections/posts.js

    Commit 8-2

    Added basic permission to check the post’s owner.

    Limitare le modifiche

    Solo per il fatto che puoi modificare i tuoi messaggi, non vuol dire che puoi modificare ogni proprietà. Per esempio, non vogliamo che gli utenti possano creare un messaggio e assegnarlo a qualcun'altro.

    deny() è la callback che in Meteor viene usata per assicurarsi che gli utenti possono modificare solamente certi campi.

    Posts = new Meteor.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);
      }
    });
    
    collections/posts.js

    Commit 8-3

    Only allow changing certain fields of posts.

    Stiamo parlando dell'array fieldNames che contiene una lista dei campi modificati, e usando il metodo without() di Underscore per ritornare un sotto-array contenente i campi che non sono url oppure title.

    Se tutto è normale, l'array dovrebbe essere vuoto e la sua lunghezza dovrebbe essere 0. Se qualcuno sta cercando di fare qualcosa di bizzarro, la lunghezza dell'array sarà 1 oppure più grande e la callback ritornerà true (di conseguenza negando l'aggiornamento).

    Chiamate dei metodi vs manipolazione dei dati sul lato client.

    Per creare dei messaggi, stiamo usando un metodo post, mentre per modificarli e cancellarli stiamo chiamando update e remove direttamente sul client, limitando l'accesso tramite allow e deny.

    Quando è opportuno uno ma non l'altro?

    Quando le cose sono relativamente semplici e puoi esprimere adeguatamente le tue regole tramite allow e deny, è solitamente più semplice fare le cose direttamente sul client.

    Manipolare i dati del database direttamente crea una percezione di immediatezza e permette di costruire una user experience migliore, fintanto che ci si ricorda di gestire i casi di fallimento in maniera graziosa (es. quando il server ritorna indietro che i cambiamenti alla fin fine non sono riusciti).

    Però è probabilmente meglio usare un metodo non appena inizi ad esigere di dover fare cose che dovrebbero essere fatte senza il controllo dell'utente (ad esempio aggiungere un timestamp per un nuovo messaggio, oppure assegnarlo all'utente giusto).

    Le chiamate ai metodi sono anche più appropriate in alcuni altri scenari:

    • Quando devi sapere o ritornare dei valori tramite callback anzichè aspettare per la reactivity (reattività) e sincronizzazione si propaghi.
    • Per delle funzioni di database pesanti che sarebbero troppo costose da inviare tramite una grande collection (collezione).
    • Per sommarizzare oppure aggregare dati (es. contare, fare la media, sommare).