Created a UI for editting a media's metadata. Had to add a new macro to
[mediagoblin.git] / mediagoblin / edit / views.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 from datetime import datetime
18
19 from itsdangerous import BadSignature
20 from werkzeug.exceptions import Forbidden
21 from werkzeug.utils import secure_filename
22
23 from mediagoblin import messages
24 from mediagoblin import mg_globals
25
26 from mediagoblin.auth import (check_password,
27 tools as auth_tools)
28 from mediagoblin.edit import forms
29 from mediagoblin.edit.lib import may_edit_media
30 from mediagoblin.decorators import (require_active_login, active_user_from_url,
31 get_media_entry_by_id, user_may_alter_collection,
32 get_user_collection, user_has_privilege,
33 user_not_banned)
34 from mediagoblin.tools.crypto import get_timed_signer_url
35 from mediagoblin.tools.mail import email_debug_message
36 from mediagoblin.tools.response import (render_to_response,
37 redirect, redirect_obj, render_404)
38 from mediagoblin.tools.translate import pass_to_ugettext as _
39 from mediagoblin.tools.template import render_template
40 from mediagoblin.tools.text import (
41 convert_to_tag_list_of_dicts, media_tags_as_string)
42 from mediagoblin.tools.url import slugify
43 from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used
44 from mediagoblin.db.models import User
45
46 import mimetypes
47
48
49 @get_media_entry_by_id
50 @require_active_login
51 def edit_media(request, media):
52 if not may_edit_media(request, media):
53 raise Forbidden("User may not edit this media")
54
55 defaults = dict(
56 title=media.title,
57 slug=media.slug,
58 description=media.description,
59 tags=media_tags_as_string(media.tags),
60 license=media.license)
61
62 form = forms.EditForm(
63 request.form,
64 **defaults)
65
66 if request.method == 'POST' and form.validate():
67 # Make sure there isn't already a MediaEntry with such a slug
68 # and userid.
69 slug = slugify(form.slug.data)
70 slug_used = check_media_slug_used(media.uploader, slug, media.id)
71
72 if slug_used:
73 form.slug.errors.append(
74 _(u'An entry with that slug already exists for this user.'))
75 else:
76 media.title = form.title.data
77 media.description = form.description.data
78 media.tags = convert_to_tag_list_of_dicts(
79 form.tags.data)
80
81 media.license = unicode(form.license.data) or None
82 media.slug = slug
83 media.save()
84
85 return redirect_obj(request, media)
86
87 if request.user.has_privilege(u'admin') \
88 and media.uploader != request.user.id \
89 and request.method != 'POST':
90 messages.add_message(
91 request, messages.WARNING,
92 _("You are editing another user's media. Proceed with caution."))
93
94 return render_to_response(
95 request,
96 'mediagoblin/edit/edit.html',
97 {'media': media,
98 'form': form})
99
100
101 # Mimetypes that browsers parse scripts in.
102 # Content-sniffing isn't taken into consideration.
103 UNSAFE_MIMETYPES = [
104 'text/html',
105 'text/svg+xml']
106
107
108 @get_media_entry_by_id
109 @require_active_login
110 def edit_attachments(request, media):
111 if mg_globals.app_config['allow_attachments']:
112 form = forms.EditAttachmentsForm()
113
114 # Add any attachements
115 if 'attachment_file' in request.files \
116 and request.files['attachment_file']:
117
118 # Security measure to prevent attachments from being served as
119 # text/html, which will be parsed by web clients and pose an XSS
120 # threat.
121 #
122 # TODO
123 # This method isn't flawless as some browsers may perform
124 # content-sniffing.
125 # This method isn't flawless as we do the mimetype lookup on the
126 # machine parsing the upload form, and not necessarily the machine
127 # serving the attachments.
128 if mimetypes.guess_type(
129 request.files['attachment_file'].filename)[0] in \
130 UNSAFE_MIMETYPES:
131 public_filename = secure_filename('{0}.notsafe'.format(
132 request.files['attachment_file'].filename))
133 else:
134 public_filename = secure_filename(
135 request.files['attachment_file'].filename)
136
137 attachment_public_filepath \
138 = mg_globals.public_store.get_unique_filepath(
139 ['media_entries', unicode(media.id), 'attachment',
140 public_filename])
141
142 attachment_public_file = mg_globals.public_store.get_file(
143 attachment_public_filepath, 'wb')
144
145 try:
146 attachment_public_file.write(
147 request.files['attachment_file'].stream.read())
148 finally:
149 request.files['attachment_file'].stream.close()
150
151 media.attachment_files.append(dict(
152 name=form.attachment_name.data \
153 or request.files['attachment_file'].filename,
154 filepath=attachment_public_filepath,
155 created=datetime.utcnow(),
156 ))
157
158 media.save()
159
160 messages.add_message(
161 request, messages.SUCCESS,
162 _("You added the attachment %s!") \
163 % (form.attachment_name.data
164 or request.files['attachment_file'].filename))
165
166 return redirect(request,
167 location=media.url_for_self(request.urlgen))
168 return render_to_response(
169 request,
170 'mediagoblin/edit/attachments.html',
171 {'media': media,
172 'form': form})
173 else:
174 raise Forbidden("Attachments are disabled")
175
176 @require_active_login
177 def legacy_edit_profile(request):
178 """redirect the old /edit/profile/?username=USER to /u/USER/edit/"""
179 username = request.GET.get('username') or request.user.username
180 return redirect(request, 'mediagoblin.edit.profile', user=username)
181
182
183 @require_active_login
184 @active_user_from_url
185 def edit_profile(request, url_user=None):
186 # admins may edit any user profile
187 if request.user.username != url_user.username:
188 if not request.user.has_privilege(u'admin'):
189 raise Forbidden(_("You can only edit your own profile."))
190
191 # No need to warn again if admin just submitted an edited profile
192 if request.method != 'POST':
193 messages.add_message(
194 request, messages.WARNING,
195 _("You are editing a user's profile. Proceed with caution."))
196
197 user = url_user
198
199 form = forms.EditProfileForm(request.form,
200 url=user.url,
201 bio=user.bio)
202
203 if request.method == 'POST' and form.validate():
204 user.url = unicode(form.url.data)
205 user.bio = unicode(form.bio.data)
206
207 user.save()
208
209 messages.add_message(request,
210 messages.SUCCESS,
211 _("Profile changes saved"))
212 return redirect(request,
213 'mediagoblin.user_pages.user_home',
214 user=user.username)
215
216 return render_to_response(
217 request,
218 'mediagoblin/edit/edit_profile.html',
219 {'user': user,
220 'form': form})
221
222 EMAIL_VERIFICATION_TEMPLATE = (
223 u'{uri}?'
224 u'token={verification_key}')
225
226
227 @require_active_login
228 def edit_account(request):
229 user = request.user
230 form = forms.EditAccountForm(request.form,
231 wants_comment_notification=user.wants_comment_notification,
232 license_preference=user.license_preference,
233 wants_notifications=user.wants_notifications)
234
235 if request.method == 'POST' and form.validate():
236 user.wants_comment_notification = form.wants_comment_notification.data
237 user.wants_notifications = form.wants_notifications.data
238
239 user.license_preference = form.license_preference.data
240
241 user.save()
242 messages.add_message(request,
243 messages.SUCCESS,
244 _("Account settings saved"))
245 return redirect(request,
246 'mediagoblin.user_pages.user_home',
247 user=user.username)
248
249 return render_to_response(
250 request,
251 'mediagoblin/edit/edit_account.html',
252 {'user': user,
253 'form': form})
254
255
256 @require_active_login
257 def delete_account(request):
258 """Delete a user completely"""
259 user = request.user
260 if request.method == 'POST':
261 if request.form.get(u'confirmed'):
262 # Form submitted and confirmed. Actually delete the user account
263 # Log out user and delete cookies etc.
264 # TODO: Should we be using MG.auth.views.py:logout for this?
265 request.session.delete()
266
267 # Delete user account and all related media files etc....
268 request.user.delete()
269
270 # We should send a message that the user has been deleted
271 # successfully. But we just deleted the session, so we
272 # can't...
273 return redirect(request, 'index')
274
275 else: # Did not check the confirmation box...
276 messages.add_message(
277 request, messages.WARNING,
278 _('You need to confirm the deletion of your account.'))
279
280 # No POST submission or not confirmed, just show page
281 return render_to_response(
282 request,
283 'mediagoblin/edit/delete_account.html',
284 {'user': user})
285
286
287 @require_active_login
288 @user_may_alter_collection
289 @get_user_collection
290 def edit_collection(request, collection):
291 defaults = dict(
292 title=collection.title,
293 slug=collection.slug,
294 description=collection.description)
295
296 form = forms.EditCollectionForm(
297 request.form,
298 **defaults)
299
300 if request.method == 'POST' and form.validate():
301 # Make sure there isn't already a Collection with such a slug
302 # and userid.
303 slug_used = check_collection_slug_used(collection.creator,
304 form.slug.data, collection.id)
305
306 # Make sure there isn't already a Collection with this title
307 existing_collection = request.db.Collection.query.filter_by(
308 creator=request.user.id,
309 title=form.title.data).first()
310
311 if existing_collection and existing_collection.id != collection.id:
312 messages.add_message(
313 request, messages.ERROR,
314 _('You already have a collection called "%s"!') % \
315 form.title.data)
316 elif slug_used:
317 form.slug.errors.append(
318 _(u'A collection with that slug already exists for this user.'))
319 else:
320 collection.title = unicode(form.title.data)
321 collection.description = unicode(form.description.data)
322 collection.slug = unicode(form.slug.data)
323
324 collection.save()
325
326 return redirect_obj(request, collection)
327
328 if request.user.has_privilege(u'admin') \
329 and collection.creator != request.user.id \
330 and request.method != 'POST':
331 messages.add_message(
332 request, messages.WARNING,
333 _("You are editing another user's collection. Proceed with caution."))
334
335 return render_to_response(
336 request,
337 'mediagoblin/edit/edit_collection.html',
338 {'collection': collection,
339 'form': form})
340
341
342 def verify_email(request):
343 """
344 Email verification view for changing email address
345 """
346 # If no token, we can't do anything
347 if not 'token' in request.GET:
348 return render_404(request)
349
350 # Catch error if token is faked or expired
351 token = None
352 try:
353 token = get_timed_signer_url("mail_verification_token") \
354 .loads(request.GET['token'], max_age=10*24*3600)
355 except BadSignature:
356 messages.add_message(
357 request,
358 messages.ERROR,
359 _('The verification key or user id is incorrect.'))
360
361 return redirect(
362 request,
363 'index')
364
365 user = User.query.filter_by(id=int(token['user'])).first()
366
367 if user:
368 user.email = token['email']
369 user.save()
370
371 messages.add_message(
372 request,
373 messages.SUCCESS,
374 _('Your email address has been verified.'))
375
376 else:
377 messages.add_message(
378 request,
379 messages.ERROR,
380 _('The verification key or user id is incorrect.'))
381
382 return redirect(
383 request, 'mediagoblin.user_pages.user_home',
384 user=user.username)
385
386
387 def change_email(request):
388 """ View to change the user's email """
389 form = forms.ChangeEmailForm(request.form)
390 user = request.user
391
392 # If no password authentication, no need to enter a password
393 if 'pass_auth' not in request.template_env.globals or not user.pw_hash:
394 form.__delitem__('password')
395
396 if request.method == 'POST' and form.validate():
397 new_email = form.new_email.data
398 users_with_email = User.query.filter_by(
399 email=new_email).count()
400
401 if users_with_email:
402 form.new_email.errors.append(
403 _('Sorry, a user with that email address'
404 ' already exists.'))
405
406 if form.password and user.pw_hash and not check_password(
407 form.password.data, user.pw_hash):
408 form.password.errors.append(
409 _('Wrong password'))
410
411 if not form.errors:
412 verification_key = get_timed_signer_url(
413 'mail_verification_token').dumps({
414 'user': user.id,
415 'email': new_email})
416
417 rendered_email = render_template(
418 request, 'mediagoblin/edit/verification.txt',
419 {'username': user.username,
420 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
421 uri=request.urlgen('mediagoblin.edit.verify_email',
422 qualified=True),
423 verification_key=verification_key)})
424
425 email_debug_message(request)
426 auth_tools.send_verification_email(user, request, new_email,
427 rendered_email)
428
429 return redirect(request, 'mediagoblin.edit.account')
430
431 return render_to_response(
432 request,
433 'mediagoblin/edit/change_email.html',
434 {'form': form,
435 'user': user})
436
437 @user_has_privilege(u'admin')
438 @require_active_login
439 @get_media_entry_by_id
440 def edit_metadata(request, media):
441 form = forms.EditMetaDataForm()
442 if media.media_metadata:
443 for row in media.media_metadata.iteritems():
444 if row[0] == "@context": continue
445 identifier = row[0]
446 # TODO Will change when we revert the metadata branch
447 value = row[1]['@value']
448 form.media_metadata.append_entry({
449 'identifier':identifier,
450 'value':value})
451 for row in media.media_metadata['@context'].iteritems():
452 identifier, value = row[0:2]
453 form.context.append_entry({
454 'identifier':identifier,
455 'value':value})
456 return render_to_response(
457 request,
458 'mediagoblin/edit/metadata.html',
459 {'form':form,
460 'media':media})