-
Notifications
You must be signed in to change notification settings - Fork 12
/
08-editing-posts.md.erb
266 lines (201 loc) · 9.95 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
261
262
263
264
265
---
title: Beiträge ändern
slug: editing-posts
date: 0008/01/01
number: 8
contents: Erstelle ein Formular für das Ändern eines Beitrags.|Richte Zugriffsberechtigungen ein.|Schränke ein, welche Felder geändert werden dürfen.
paragraphs: 29
---
Da wir nun Beiträge anlegen können, ist der nächste Schritt, sie änderbar und löschbar zu machen. Der Code für das UI ist relativ einfach, deshalb nutzen wir die Gelegenheit, um in diesem Kapitel darüber zu sprechen, wie Meteor mit Benutzerberechtigungen umgeht.
Im ersten Schritt erweitern wir unseren Router. Wir fügen eine Route zur Seite zum Ändern von Beitragen ein und setzen den Kontext dieser Seite entsprechend.
~~~js
Router.configure({
layoutTemplate: 'layout'
});
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() {
if (! Meteor.user()) {
if (Meteor.loggingIn())
this.render('loading')
else
this.render('accessDenied');
this.stop();
}
}
Router.before(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "12~15" %>
### Das Template für die Beitragsänderung
Jetzt können wir uns auf das dazugehörige Template konzentrieren. Unser Template `postEdit` wird ein ziemlich gewöhnliches Formular:
~~~html
<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>
~~~
<%= caption "client/views/posts/post_edit.html" %>
Und hier ist der dazu gehörige Manager `post_edit.js`:
~~~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/views/posts/post_edit.js" %>
Mittlerweile sollte dir der meiste Code bekannt vorkommen.
Wir haben zwei Event-Callbacks für das Template definiert: eines für das `submit` Event des Formulars und eines für das `click` Event des Löschen-Links.
Der Callback zum Löschen ist sehr einfach aufgebaut: Nach dem Unterdrücken des Default-Events, wird eine Bestätigung durch den Benutzer angefordert. Wenn wir diese erhalten, holen wir die ID des aktuellen Beitrags aus dem Kontext und löschen den Beitrag unter Verwendung dieser ID. Am Ende wird der Benutzer zur Homepage geleitet.
Der Callback zum Aktualisieren ist ein wenig länger, aber nicht wesentlich komplizierter: Nach dem Unterdrücken des Default-Events und dem Ermitteln der ID des Beitrags, entnehmen wir die neuen Werte aus dem Zielobjekt des Events und speichern diese in einem Objekt namens `postProperties`.
Dieses Objekt übergeben wir an die Meteor-Methode `Collection.update()`, zusammen mit einem Callback als letzten Parameter der Methode. Der Callback wird am Ende der Operation ausgeführt. Im Fehlerfall wird der Grund für den Fehler ausgegeben. Im Falle eines Erfolgs leitet der Callback den Benutzer auf die Ansichts-Seite des Beitrags zurück.
### Hinzufügen von Links
Es fehlen noch Berarbeiten-Links in unseren Beiträgen. Erst mit diesen, haben unsere Benutzer die Möglichkeit die Seite für das Ändern von Beiträgen zu erreichen.
~~~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">Discuss</a>
</div>
</template>
~~~
<%= caption "client/views/posts/post_item.html" %>
<%= highlight "5~8" %>
Natürlich wollen wir nur einen Bearbeiten-Link anzeigen, wenn dem Benutzer der Beitrag auch gehört. Das erledigen wir mit dem Helper `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/views/posts/post_item.js" %>
<%= highlight "2~4" %>
<%= screenshot "8-1", "Formular für das Ändern von Beiträgen." %>
<%= commit "8-1", "Added edit posts form." %>
Unser Änderungsformular für Beiträge sieht gut aus. Aber das eigentliche Ändern der Beiträge ist noch gar nicht möglich. Warum ist das so?
### Einrichtung der Berechtigungen
Da wir im letzten Kapitel das Package `insecure` entfernt haben, werden alle client-seitigen Änderungen derzeit abgewiesen.
Um Änderungen wieder zu ermöglichen, werden wir Berechtigungsregeln anlegen. Zunächst erzeuge die neue Datei `permissions.js` im Verzeichnis `lib`. Diese wird unsere Berechtigungslogik als Erstes laden und ist sowohl auf dem Server als auch auf dem Client verfügbar.
~~~js
// check that the userId specified owns the documents
ownsDocument = function(userId, doc) {
return doc && doc.userId === userId;
}
~~~
<%= caption "lib/permissions.js" %>
Im Kapitel [Beiträge anlegen](/chapter/creating-posts), haben wir die Methode `allow()` entfernt, da wir neue Posts nur noch per Server-Methode angelegt haben (auf diese Weise umgehen wir `allow()` sowieso).
Aber jetzt, wo wir Beiträge im Client ändern und löschen, schauen wir uns noch mal die Datei `posts.js` an. Wir fügen den folgenden Block `allow()` wieder hinzu:
~~~js
Posts = new Meteor.Collection('posts');
Posts.allow({
update: ownsDocument,
remove: ownsDocument
});
Meteor.methods({
...
~~~
<%= caption "collections/posts.js" %>
<%= highlight "3~6" %>
<%= commit "8-2", "Added basic permission to check the post's owner." %>
### Einschränkung von Änderungen
Nur weil du deine Beiträge ändern darfst soll das nicht heissen, dass dies für jede Eigenschaft des Beitrags gilt. Zum Beispiel wollen wir nicht, dass ein Benutzer einen Beiträg erstellen kann und ihn dann jemand anderem zuweist.
Wir benutzen hierfür Meteors Callback `deny()` um sicherzustellen, dass Benutzer nur angegebene Felder ändern können:
~~~js
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);
}
});
~~~
<%= caption "collections/posts.js" %>
<%= highlight "8~13" %>
<%= commit "8-3", "Only allow changing certain fields of posts." %>
Als Parameter erhalten wir das Array `fieldNames`, dass eine Liste der Felder enthält, die geändert werden sollen. Durch die Verwendung von [Underscore](http://underscorejs.org/)s Methode `without()` reduzieren wir das Array um die Werte `url` und `title`.
Im Normalfall sollte dieses Ergebnis-Array leer sein, denn nur `url` und `title` sollen geändert werden können. Wenn jemand etwas komisches probiert, wird die Länge des Arrays grösser gleich 1 sein. Dann liefert der Callback den Wert `true` zurück (und verhindert die Änderung).
<% note do %>
### Methodenaufrufe vs. Clientseitige Datenmanipulation
Um Beiträge zu erzeugen, benutzen wir die Server-Methode `post`. Aber zum Ändern und Löschen, rufen wir die Methoden `update` und `remove` direkt auf dem Client auf und regeln die Berechtigung mit `allow` und `deny`.
Wann ist es sinnvoll das Eine und wann das Andere zu verwenden?
Wenn der Sachverhalt unkompliziert ist und du die Regeln mit `allow` und `deny` adequat festlegen kannst, ist es normalerweise einfacher und weniger aufwendig, diese Dinge direkt auf dem Client zu regeln.
Das Manipulieren der Daten auf dem Client trägt zum Gefühl von Direktheit und einem besseren Benutzererlebnis bei. Fehlerzustände müssen aber gesondert berücksichtigt und elegant eingeflochten werden. Zum Beispiel: der Server meldet sich asynchron zurück und teilt mit, dass die Änderung doch nicht stattgefunden hat.
Aber, die Erstellung und Änderung von Daten, die der Benutzer nicht direkt manipulieren darf (zum Beispiel die Vergabe von Zeitstempeln oder die Zuordnung eines Benutzers), sollten besser in einer Server-Methode erfolgen.
Server-Methoden sind auch in folgenden Szenarien eher angebracht:
- Wenn du Daten und Ergebnisse direkt per Callback benötigst und nicht darauf warten willst, dass diese über den Reaktivitätsmechanismus synchronisiert werden.
- Für Datenbankoperationen, bei denen viele Elemente geändert werden und damit eine große Menge an Daten übertragen werden muss.
- Um Daten zusammenzufassen oder zu aggregieren (z.B. Anzahl der Elemente, Mittelwerte oder Summen)
<% end %>