Set up the metadata editor forms
[mediagoblin.git] / mediagoblin / edit / views.py
CommitLineData
9bfe1d8e 1# GNU MediaGoblin -- federated, autonomous media hosting
cf29e8a8 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
9bfe1d8e
E
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/>.
aba81c9f 16
3a8c3a38
JW
17from datetime import datetime
18
377db0e7 19from itsdangerous import BadSignature
62d14bf5 20from werkzeug.exceptions import Forbidden
3a8c3a38 21from werkzeug.utils import secure_filename
aba81c9f 22
d9ed098e 23from mediagoblin import messages
10d7496d 24from mediagoblin import mg_globals
152a3bfa 25
201ac388
CAW
26from mediagoblin.auth import (check_password,
27 tools as auth_tools)
aba81c9f 28from mediagoblin.edit import forms
b5a64f78 29from mediagoblin.edit.lib import may_edit_media
abc4da29 30from mediagoblin.decorators import (require_active_login, active_user_from_url,
89e1563f 31 get_media_entry_by_id, user_may_alter_collection,
fffc5dcf 32 get_user_collection, user_has_privilege,
33 user_not_banned)
89e1563f 34from mediagoblin.tools.crypto import get_timed_signer_url
af4414a8 35from mediagoblin.tools.mail import email_debug_message
89e1563f
RE
36from mediagoblin.tools.response import (render_to_response,
37 redirect, redirect_obj, render_404)
5ae0cbaa 38from mediagoblin.tools.translate import pass_to_ugettext as _
89e1563f 39from mediagoblin.tools.template import render_template
152a3bfa 40from mediagoblin.tools.text import (
a855e92a 41 convert_to_tag_list_of_dicts, media_tags_as_string)
9e9d9083 42from mediagoblin.tools.url import slugify
be5be115 43from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used
89e1563f 44from mediagoblin.db.models import User
c849e690 45
825b1362
JW
46import mimetypes
47
97ec97db 48
461dd971 49@get_media_entry_by_id
aba81c9f
E
50@require_active_login
51def edit_media(request, media):
c849e690 52 if not may_edit_media(request, media):
cfa92229 53 raise Forbidden("User may not edit this media")
c849e690 54
2c437493 55 defaults = dict(
ec82fbd8 56 title=media.title,
5da0bf90 57 slug=media.slug,
1d939966 58 description=media.description,
a6c49d49 59 tags=media_tags_as_string(media.tags),
97ec97db 60 license=media.license)
aba81c9f 61
2c437493 62 form = forms.EditForm(
111a609d 63 request.form,
2c437493
JW
64 **defaults)
65
98857207 66 if request.method == 'POST' and form.validate():
d5e90fe4
CAW
67 # Make sure there isn't already a MediaEntry with such a slug
68 # and userid.
dc03850b 69 slug = slugify(form.slug.data)
9e9d9083 70 slug_used = check_media_slug_used(media.uploader, slug, media.id)
3a8c3a38 71
b62b3b98 72 if slug_used:
d5e90fe4 73 form.slug.errors.append(
4b1adc13 74 _(u'An entry with that slug already exists for this user.'))
d5e90fe4 75 else:
dc03850b
HL
76 media.title = form.title.data
77 media.description = form.description.data
de917303 78 media.tags = convert_to_tag_list_of_dicts(
dc03850b 79 form.tags.data)
3a8c3a38 80
dc03850b 81 media.license = unicode(form.license.data) or None
9e9d9083 82 media.slug = slug
747623cc 83 media.save()
d5e90fe4 84
2e6ee596 85 return redirect_obj(request, media)
98857207 86
8394febb 87 if request.user.has_privilege(u'admin') \
5c2b8486 88 and media.uploader != request.user.id \
96a2c366
CAW
89 and request.method != 'POST':
90 messages.add_message(
91 request, messages.WARNING,
4b1adc13 92 _("You are editing another user's media. Proceed with caution."))
96a2c366 93
9038c9f9
CAW
94 return render_to_response(
95 request,
c9c24934
E
96 'mediagoblin/edit/edit.html',
97 {'media': media,
98 'form': form})
46fd661e 99
3a8c3a38 100
825b1362
JW
101# Mimetypes that browsers parse scripts in.
102# Content-sniffing isn't taken into consideration.
103UNSAFE_MIMETYPES = [
104 'text/html',
105 'text/svg+xml']
106
107
954b407c 108@get_media_entry_by_id
630b57a3 109@require_active_login
3a8c3a38
JW
110def edit_attachments(request, media):
111 if mg_globals.app_config['allow_attachments']:
112 form = forms.EditAttachmentsForm()
113
114 # Add any attachements
c43f8c1d
JW
115 if 'attachment_file' in request.files \
116 and request.files['attachment_file']:
3a8c3a38 117
825b1362
JW
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(
c43f8c1d 129 request.files['attachment_file'].filename)[0] in \
825b1362
JW
130 UNSAFE_MIMETYPES:
131 public_filename = secure_filename('{0}.notsafe'.format(
c43f8c1d 132 request.files['attachment_file'].filename))
825b1362
JW
133 else:
134 public_filename = secure_filename(
c43f8c1d 135 request.files['attachment_file'].filename)
825b1362 136
3a8c3a38
JW
137 attachment_public_filepath \
138 = mg_globals.public_store.get_unique_filepath(
5c2b8486 139 ['media_entries', unicode(media.id), 'attachment',
825b1362 140 public_filename])
3a8c3a38
JW
141
142 attachment_public_file = mg_globals.public_store.get_file(
143 attachment_public_filepath, 'wb')
144
145 try:
146 attachment_public_file.write(
c43f8c1d 147 request.files['attachment_file'].stream.read())
3a8c3a38 148 finally:
c43f8c1d 149 request.files['attachment_file'].stream.close()
3a8c3a38 150
35029581 151 media.attachment_files.append(dict(
dc03850b 152 name=form.attachment_name.data \
c43f8c1d 153 or request.files['attachment_file'].filename,
3a8c3a38 154 filepath=attachment_public_filepath,
243c3843 155 created=datetime.utcnow(),
3a8c3a38 156 ))
630b57a3 157
3a8c3a38
JW
158 media.save()
159
160 messages.add_message(
161 request, messages.SUCCESS,
32255ec0 162 _("You added the attachment %s!") \
dc03850b 163 % (form.attachment_name.data
c43f8c1d 164 or request.files['attachment_file'].filename))
3a8c3a38 165
950124e6
SS
166 return redirect(request,
167 location=media.url_for_self(request.urlgen))
3a8c3a38
JW
168 return render_to_response(
169 request,
170 'mediagoblin/edit/attachments.html',
171 {'media': media,
172 'form': form})
173 else:
cfa92229 174 raise Forbidden("Attachments are disabled")
3a8c3a38 175
abc4da29
SS
176@require_active_login
177def 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
3a8c3a38
JW
182
183@require_active_login
abc4da29
SS
184@active_user_from_url
185def edit_profile(request, url_user=None):
186 # admins may edit any user profile
187 if request.user.username != url_user.username:
8394febb 188 if not request.user.has_privilege(u'admin'):
abc4da29
SS
189 raise Forbidden(_("You can only edit your own profile."))
190
a0cf14fe
CFD
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,
4b1adc13 195 _("You are editing a user's profile. Proceed with caution."))
abc4da29
SS
196
197 user = url_user
a0cf14fe 198
111a609d 199 form = forms.EditProfileForm(request.form,
066d49b2
SS
200 url=user.url,
201 bio=user.bio)
630b57a3 202
203 if request.method == 'POST' and form.validate():
dc03850b
HL
204 user.url = unicode(form.url.data)
205 user.bio = unicode(form.bio.data)
4c465852 206
c8071fa5 207 user.save()
630b57a3 208
c8071fa5
JS
209 messages.add_message(request,
210 messages.SUCCESS,
211 _("Profile changes saved"))
212 return redirect(request,
213 'mediagoblin.user_pages.user_home',
703d09b9 214 user=user.username)
630b57a3 215
216 return render_to_response(
217 request,
218 'mediagoblin/edit/edit_profile.html',
219 {'user': user,
220 'form': form})
c8071fa5 221
89e1563f
RE
222EMAIL_VERIFICATION_TEMPLATE = (
223 u'{uri}?'
224 u'token={verification_key}')
225
c8071fa5
JS
226
227@require_active_login
228def edit_account(request):
c8071fa5 229 user = request.user
111a609d 230 form = forms.EditAccountForm(request.form,
066d49b2 231 wants_comment_notification=user.wants_comment_notification,
93d805ad
RE
232 license_preference=user.license_preference,
233 wants_notifications=user.wants_notifications)
c8071fa5 234
89e1563f 235 if request.method == 'POST' and form.validate():
f670f48d 236 user.wants_comment_notification = form.wants_comment_notification.data
93d805ad 237 user.wants_notifications = form.wants_notifications.data
dc4dfbde 238
f670f48d 239 user.license_preference = form.license_preference.data
dc4dfbde 240
dd57c6c5
RE
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)
630b57a3 248
249 return render_to_response(
250 request,
c8071fa5 251 'mediagoblin/edit/edit_account.html',
630b57a3 252 {'user': user,
253 'form': form})
be5be115
AW
254
255
380f22b8
SS
256@require_active_login
257def 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
be5be115
AW
287@require_active_login
288@user_may_alter_collection
289@get_user_collection
290def edit_collection(request, collection):
291 defaults = dict(
292 title=collection.title,
293 slug=collection.slug,
294 description=collection.description)
295
296 form = forms.EditCollectionForm(
111a609d 297 request.form,
be5be115
AW
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.
455fd36f 303 slug_used = check_collection_slug_used(collection.creator,
dc03850b 304 form.slug.data, collection.id)
c43f8c1d 305
be5be115 306 # Make sure there isn't already a Collection with this title
44082b12
RE
307 existing_collection = request.db.Collection.query.filter_by(
308 creator=request.user.id,
309 title=form.title.data).first()
c43f8c1d 310
be5be115
AW
311 if existing_collection and existing_collection.id != collection.id:
312 messages.add_message(
a6481028
CAW
313 request, messages.ERROR,
314 _('You already have a collection called "%s"!') % \
dc03850b 315 form.title.data)
be5be115
AW
316 elif slug_used:
317 form.slug.errors.append(
318 _(u'A collection with that slug already exists for this user.'))
319 else:
dc03850b
HL
320 collection.title = unicode(form.title.data)
321 collection.description = unicode(form.description.data)
322 collection.slug = unicode(form.slug.data)
be5be115
AW
323
324 collection.save()
325
2e6ee596 326 return redirect_obj(request, collection)
be5be115 327
8394febb 328 if request.user.has_privilege(u'admin') \
5c2b8486 329 and collection.creator != request.user.id \
be5be115
AW
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})
39aa1db4
RE
340
341
89e1563f
RE
342def 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
377db0e7
RE
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')
89e1563f
RE
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)
5adb906a
RE
385
386
402f4360
RE
387def 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
201ac388 406 if form.password and user.pw_hash and not check_password(
402f4360
RE
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})
fffc5dcf 436
437@user_has_privilege(u'admin')
438@require_active_login
439@get_media_entry_by_id
440def edit_metadata(request, media):
441 form = forms.EditMetaDataForm()
442 return render_to_response(
443 request,
444 'mediagoblin/edit/metadata.html',
445 {'form':form})