-
Notifications
You must be signed in to change notification settings - Fork 30
/
08-editing-posts.md.erb
260 lines (198 loc) · 9.82 KB
/
08-editing-posts.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
---
title: Editando posts
slug: editing-posts
date: 0008/01/01
number: 8
points: 5
photoUrl: http://www.flickr.com/photos/ikewinski/9473337133/
photoAuthor: Mike Lewinski
contents: Añadiremos un formulario para editar posts.|Configuraremos los permisos de edición.|Restringiremos las propiedades que se pueden editar.
paragraphs: 29
---
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:
~~~js
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'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "15~18" %>
### La plantilla de edición de posts
Ahora ya nos podemos centrar en la plantilla. Nuestra plantilla `postEdit` tendrá una forma bastante estándar:
~~~html
<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>
~~~
<%= caption "client/templates/posts/post_edit.html" %>
Y aquí tenemos el fichero `post_edit.js` que la acompaña:
~~~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');
}
}
});
~~~
<%= caption "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`](http://docs.mongodb.org/manual/reference/operator/update/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:
~~~html
<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>
~~~
<%= caption "client/templates/posts/post_item.html" %>
<%= highlight "5~8" %>
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`:
~~~js
Template.postItem.helpers({
ownPost: function() {
return this.userId === Meteor.userId();
},
domain: function() {
var a = document.createElement('a');
a.href = this.url;
return a.hostname;
}
});
~~~
<%= caption "client/templates/posts/post_item.js" %>
<%= highlight "2~4" %>
<%= screenshot "8-1", "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):
~~~js
// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
return doc && doc.userId === userId;
}
~~~
<%= caption "lib/permissions.js" %>
En el capítulo [Creando posts](/chapter/creating-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()`:
~~~js
Posts = new Mongo.Collection('posts');
Posts.allow({
update: function(userId, post) { return ownsDocument(userId, post); },
remove: function(userId, post) { return ownsDocument(userId, post); },
});
//...
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~6" %>
<%= 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:
~~~js
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);
}
});
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "8~13" %>
<%= 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](http://underscorejs.org/) 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.
<% note do %>
### 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](https://www.discovermeteor.com/blog/meteor-methods-client-side-operations/).
<% end %>