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