diff options
30 files changed, 841 insertions, 177 deletions
diff --git a/docs/hackinghowto.rst b/docs/hackinghowto.rst index a56498bb..a9aadb62 100644 --- a/docs/hackinghowto.rst +++ b/docs/hackinghowto.rst @@ -152,7 +152,7 @@ Running the test suite Run:: - ./bin/nosetests + CELERY_CONFIG_MODULE=mediagoblin.celery_setup.from_tests ./bin/nosetests Running a shell diff --git a/mediagoblin/app.py b/mediagoblin/app.py index 714404de..e5949531 100644 --- a/mediagoblin/app.py +++ b/mediagoblin/app.py @@ -139,12 +139,13 @@ def paste_app_factory(global_config, **app_config): raise ImproperlyConfigured( "One of direct_remote_path or direct_remote_paths must be provided") - if asbool(os.environ.get('CELERY_ALWAYS_EAGER')): - setup_celery_from_config( - app_config, global_config, - force_celery_always_eager=True) - else: - setup_celery_from_config(app_config, global_config) + if not asbool(app_config.get('celery_setup_elsewhere')): + if asbool(os.environ.get('CELERY_ALWAYS_EAGER')): + setup_celery_from_config( + app_config, global_config, + force_celery_always_eager=True) + else: + setup_celery_from_config(app_config, global_config) mgoblin_app = MediaGoblinApp( connection, db, diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py index db8aaceb..7bc0aeb1 100644 --- a/mediagoblin/auth/forms.py +++ b/mediagoblin/auth/forms.py @@ -27,7 +27,9 @@ class RegistrationForm(wtforms.Form): 'Password', [wtforms.validators.Required(), wtforms.validators.Length(min=6, max=30), - wtforms.validators.EqualTo('confirm_password')]) + wtforms.validators.EqualTo( + 'confirm_password', + 'Passwords must match.')]) confirm_password = wtforms.PasswordField( 'Confirm password', [wtforms.validators.Required()]) diff --git a/mediagoblin/auth/lib.py b/mediagoblin/auth/lib.py index 7cf021bc..f40e560f 100644 --- a/mediagoblin/auth/lib.py +++ b/mediagoblin/auth/lib.py @@ -19,6 +19,9 @@ import random import bcrypt +from mediagoblin.util import send_email, render_template +from mediagoblin import globals as mgoblin_globals + def bcrypt_check_password(raw_pass, stored_hash, extra_salt=None): """ @@ -84,3 +87,37 @@ def fake_login_attempt(): randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt) randplus_stored_hash == randplus_hashed_pass + + +EMAIL_VERIFICATION_TEMPLATE = ( + u"http://{host}{uri}?" + u"userid={userid}&token={verification_key}") + +def send_verification_email(user, request): + """ + Send the verification email to users to activate their accounts. + + Args: + - user: a user object + - request: the request + """ + rendered_email = render_template( + request, 'mediagoblin/auth/verification_email.txt', + {'username': user['username'], + 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format( + host=request.host, + uri=request.urlgen('mediagoblin.auth.verify_email'), + userid=unicode(user['_id']), + verification_key=user['verification_key'])}) + + # TODO: There is no error handling in place + send_email( + mgoblin_globals.email_sender_address, + [user['email']], + # TODO + # Due to the distributed nature of GNU MediaGoblin, we should + # find a way to send some additional information about the + # specific GNU MediaGoblin instance in the subject line. For + # example "GNU MediaGoblin @ Wandborg - [...]". + 'GNU MediaGoblin - Verify your email!', + rendered_email) diff --git a/mediagoblin/auth/routing.py b/mediagoblin/auth/routing.py index a8909fbb..46c585d2 100644 --- a/mediagoblin/auth/routing.py +++ b/mediagoblin/auth/routing.py @@ -20,7 +20,8 @@ auth_routes = [ Route('mediagoblin.auth.register', '/register/', controller='mediagoblin.auth.views:register'), Route('mediagoblin.auth.register_success', '/register/success/', - controller='mediagoblin.auth.views:register_success'), + template='mediagoblin/auth/register_success.html', + controller='mediagoblin.views:simple_template_render'), Route('mediagoblin.auth.login', '/login/', controller='mediagoblin.auth.views:login'), Route('mediagoblin.auth.logout', '/logout/', @@ -28,9 +29,11 @@ auth_routes = [ Route('mediagoblin.auth.verify_email', '/verify_email/', controller='mediagoblin.auth.views:verify_email'), Route('mediagoblin.auth.verify_email_notice', '/verification_required/', - controller='mediagoblin.auth.views:verify_email_notice'), + template='mediagoblin/auth/verification_needed.html', + controller='mediagoblin.views:simple_template_render'), Route('mediagoblin.auth.resend_verification', '/resend_verification/', controller='mediagoblin.auth.views:resend_activation'), Route('mediagoblin.auth.resend_verification_success', '/resend_verification_success/', - controller='mediagoblin.auth.views:resend_activation_success')] + template='mediagoblin/auth/resent_verification_email.html', + controller='mediagoblin.views:simple_template_render')] diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index e9d75f8b..1d00f382 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -16,13 +16,13 @@ import uuid -from webob import Response, exc +from webob import exc +from mediagoblin.util import render_to_response, redirect from mediagoblin.db.util import ObjectId from mediagoblin.auth import lib as auth_lib from mediagoblin.auth import forms as auth_forms -from mediagoblin.util import send_email -from mediagoblin import globals as mgoblin_globals +from mediagoblin.auth.lib import send_verification_email def register(request): @@ -52,46 +52,14 @@ def register(request): request.POST['password']) entry.save(validate=True) - email_template = request.template_env.get_template( - 'mediagoblin/auth/verification_email.txt') - - # TODO: There is no error handling in place - send_email( - mgoblin_globals.email_sender_address, - [entry['email']], - # TODO - # Due to the distributed nature of GNU MediaGoblin, we should - # find a way to send some additional information about the - # specific GNU MediaGoblin instance in the subject line. For - # example "GNU MediaGoblin @ Wandborg - [...]". - 'GNU MediaGoblin - Verify email', - email_template.render( - username=entry['username'], - verification_url='http://{host}{uri}?userid={userid}&token={verification_key}'.format( - host=request.host, - uri=request.urlgen('mediagoblin.auth.verify_email'), - userid=unicode(entry['_id']), - verification_key=entry['verification_key']))) - - # Redirect to register_success - return exc.HTTPFound( - location=request.urlgen("mediagoblin.auth.register_success")) - - # render - template = request.template_env.get_template( - 'mediagoblin/auth/register.html') - return Response( - template.render( - {'request': request, - 'register_form': register_form})) + send_verification_email(entry, request) + return redirect(request, "mediagoblin.auth.register_success") -def register_success(request): - template = request.template_env.get_template( - 'mediagoblin/auth/register_success.html') - return Response( - template.render( - {'request': request})) + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form}) def login(request): @@ -116,8 +84,7 @@ def login(request): if request.POST.get('next'): return exc.HTTPFound(location=request.POST['next']) else: - return exc.HTTPFound( - location=request.urlgen("index")) + return redirect(request, "index") else: # Prevent detecting who's on this system by testing login @@ -125,23 +92,19 @@ def login(request): auth_lib.fake_login_attempt() login_failed = True - # render - template = request.template_env.get_template( - 'mediagoblin/auth/login.html') - return Response( - template.render( - {'request': request, - 'login_form': login_form, - 'next': request.GET.get('next') or request.POST.get('next'), - 'login_failed': login_failed})) + return render_to_response( + request, + 'mediagoblin/auth/login.html', + {'login_form': login_form, + 'next': request.GET.get('next') or request.POST.get('next'), + 'login_failed': login_failed}) def logout(request): # Maybe deleting the user_id parameter would be enough? request.session.delete() - return exc.HTTPFound( - location=request.urlgen("index")) + return redirect(request, "index") def verify_email(request): @@ -158,8 +121,6 @@ def verify_email(request): user = request.db.User.find_one( {'_id': ObjectId(unicode(request.GET['userid']))}) - verification_successful = bool - if user and user['verification_key'] == unicode(request.GET['token']): user['status'] = u'active' user['email_verified'] = True @@ -168,27 +129,11 @@ def verify_email(request): else: verification_successful = False - template = request.template_env.get_template( - 'mediagoblin/auth/verify_email.html') - return Response( - template.render( - {'request': request, - 'user': user, - 'verification_successful': verification_successful})) - -def verify_email_notice(request): - """ - Verify warning view. - - When the user tries to do some action that requires their account - to be verified beforehand, this view is called upon! - """ - - template = request.template_env.get_template( - 'mediagoblin/auth/verification_needed.html') - return Response( - template.render( - {'request': request})) + return render_to_response( + request, + 'mediagoblin/auth/verify_email.html', + {'user': user, + 'verification_successful': verification_successful}) def resend_activation(request): @@ -200,36 +145,6 @@ def resend_activation(request): request.user['verification_key'] = unicode(uuid.uuid4()) request.user.save() - # Copied shamelessly from the register view above. - - email_template = request.template_env.get_template( - 'mediagoblin/auth/verification_email.txt') - - # TODO: There is no error handling in place - send_email( - mgoblin_globals.email_sender_address, - [request.user['email']], - # TODO - # Due to the distributed nature of GNU MediaGoblin, we should - # find a way to send some additional information about the - # specific GNU MediaGoblin instance in the subject line. For - # example "GNU MediaGoblin @ Wandborg - [...]". - 'GNU MediaGoblin - Verify email', - email_template.render( - username=request.user['username'], - verification_url='http://{host}{uri}?userid={userid}&token={verification_key}'.format( - host=request.host, - uri=request.urlgen('mediagoblin.auth.verify_email'), - userid=unicode(request.user['_id']), - verification_key=request.user['verification_key']))) - - return exc.HTTPFound( - location=request.urlgen('mediagoblin.auth.resend_verification_success')) - - -def resend_activation_success(request): - template = request.template_env.get_template( - 'mediagoblin/auth/resent_verification_email.html') - return Response( - template.render( - {'request': request})) + send_verification_email(request.user, request) + + return redirect(request, 'mediagoblin.auth.resend_verification_success') diff --git a/mediagoblin/celery_setup/__init__.py b/mediagoblin/celery_setup/__init__.py index 1a77cc62..d4f25b07 100644 --- a/mediagoblin/celery_setup/__init__.py +++ b/mediagoblin/celery_setup/__init__.py @@ -140,6 +140,7 @@ def setup_celery_from_config(app_config, global_config, if force_celery_always_eager: celery_settings['CELERY_ALWAYS_EAGER'] = True + celery_settings['CELERY_EAGER_PROPAGATES_EXCEPTIONS'] = True __import__(settings_module) this_module = sys.modules[settings_module] diff --git a/mediagoblin/celery_setup/from_tests.py b/mediagoblin/celery_setup/from_tests.py new file mode 100644 index 00000000..fe7d7314 --- /dev/null +++ b/mediagoblin/celery_setup/from_tests.py @@ -0,0 +1,43 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from mediagoblin.tests.tools import TEST_APP_CONFIG +from mediagoblin import util +from mediagoblin.celery_setup import setup_celery_from_config +from mediagoblin.globals import setup_globals + + +OUR_MODULENAME = 'mediagoblin.celery_setup.from_tests' + + +def setup_self(setup_globals_func=setup_globals): + """ + Set up celery for testing's sake, which just needs to set up + celery and celery only. + """ + mgoblin_conf = util.read_config_file(TEST_APP_CONFIG) + mgoblin_section = mgoblin_conf['app:mediagoblin'] + + setup_celery_from_config( + mgoblin_section, mgoblin_conf, + settings_module=OUR_MODULENAME, + set_environ=False) + + +if os.environ.get('CELERY_CONFIG_MODULE') == OUR_MODULENAME: + setup_self() diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py index a5bede54..c2fe3f9f 100644 --- a/mediagoblin/decorators.py +++ b/mediagoblin/decorators.py @@ -18,6 +18,7 @@ from bson.errors import InvalidId from webob import exc +from mediagoblin.util import redirect from mediagoblin.db.util import ObjectId @@ -38,9 +39,8 @@ def require_active_login(controller): def new_controller_func(request, *args, **kwargs): if request.user and \ request.user.get('status') == u'needs_email_verification': - return exc.HTTPFound( - location = request.urlgen( - 'mediagoblin.auth.verify_email_notice')) + return redirect(request, + 'mediagoblin.auth.verify_email_notice') elif not request.user or request.user.get('status') != u'active': return exc.HTTPFound( location="%s?next=%s" % ( @@ -102,3 +102,23 @@ def get_user_media_entry(controller): return controller(request, media=media, *args, **kwargs) return _make_safe(wrapper, controller) + +def get_media_entry_by_id(controller): + """ + Pass in a MediaEntry based off of a url component + """ + def wrapper(request, *args, **kwargs): + try: + media = request.db.MediaEntry.find_one( + {'_id': ObjectId(request.matchdict['media']), + 'state': 'processed'}) + except InvalidId: + return exc.HTTPNotFound() + + # Still no media? Okay, 404. + if not media: + return exc.HTTPNotFound() + + return controller(request, media=media, *args, **kwargs) + + return _make_safe(wrapper, controller) diff --git a/mediagoblin/edit/__init__.py b/mediagoblin/edit/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/mediagoblin/edit/__init__.py diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py new file mode 100644 index 00000000..ea25141d --- /dev/null +++ b/mediagoblin/edit/forms.py @@ -0,0 +1,27 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import wtforms + + +class EditForm(wtforms.Form): + title = wtforms.TextField( + 'Title', + [wtforms.validators.Length(min=0, max=500)]) + slug = wtforms.TextField( + 'Slug') + description = wtforms.TextAreaField('Description of this work') diff --git a/mediagoblin/edit/lib.py b/mediagoblin/edit/lib.py new file mode 100644 index 00000000..2a810349 --- /dev/null +++ b/mediagoblin/edit/lib.py @@ -0,0 +1,24 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +def may_edit_media(request, media): + """Check, if the request's user may edit the media details""" + if media['uploader'] == request.user['_id']: + return True + if request.user['is_admin']: + return True + return False diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py new file mode 100644 index 00000000..bf0b2498 --- /dev/null +++ b/mediagoblin/edit/routing.py @@ -0,0 +1,22 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +from routes.route import Route + +edit_routes = [ + # Media editing view handled in user_pages/routing.py +] diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py new file mode 100644 index 00000000..c5f0f435 --- /dev/null +++ b/mediagoblin/edit/views.py @@ -0,0 +1,61 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +from webob import exc + +from mediagoblin.util import render_to_response, redirect +from mediagoblin.edit import forms +from mediagoblin.edit.lib import may_edit_media +from mediagoblin.decorators import require_active_login, get_user_media_entry + + +@get_user_media_entry +@require_active_login +def edit_media(request, media): + if not may_edit_media(request, media): + return exc.HTTPForbidden() + + form = forms.EditForm(request.POST, + title = media['title'], + slug = media['slug'], + description = media['description']) + + if request.method == 'POST' and form.validate(): + # Make sure there isn't already a MediaEntry with such a slug + # and userid. + existing_user_slug_entries = request.db.MediaEntry.find( + {'slug': request.POST['slug'], + 'uploader': media['uploader'], + '_id': {'$ne': media['_id']}}).count() + + if existing_user_slug_entries: + form.slug.errors.append( + u'An entry with that slug already exists for this user.') + else: + media['title'] = request.POST['title'] + media['description'] = request.POST['description'] + media['slug'] = request.POST['slug'] + media.save() + + return redirect(request, "mediagoblin.user_pages.media_home", + user=media.uploader()['username'], media=media['slug']) + + return render_to_response( + request, + 'mediagoblin/edit/edit.html', + {'media': media, + 'form': form}) diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py index 356ef678..b854c85a 100644 --- a/mediagoblin/routing.py +++ b/mediagoblin/routing.py @@ -19,6 +19,7 @@ from routes import Mapper from mediagoblin.auth.routing import auth_routes from mediagoblin.submit.routing import submit_routes from mediagoblin.user_pages.routing import user_routes +from mediagoblin.edit.routing import edit_routes def get_mapper(): mapping = Mapper() @@ -31,5 +32,6 @@ def get_mapper(): mapping.extend(auth_routes, '/auth') mapping.extend(submit_routes, '/submit') mapping.extend(user_routes, '/u') + mapping.extend(edit_routes, '/edit') return mapping diff --git a/mediagoblin/submit/routing.py b/mediagoblin/submit/routing.py index cff28acb..3edbab70 100644 --- a/mediagoblin/submit/routing.py +++ b/mediagoblin/submit/routing.py @@ -20,5 +20,5 @@ submit_routes = [ Route('mediagoblin.submit.start', '/', controller='mediagoblin.submit.views:submit_start'), Route('mediagoblin.submit.success', '/success/', - controller='mediagoblin.submit.views:submit_success'), - ] + template='mediagoblin/submit/success.html', + controller='mediagoblin.views:simple_template_render')] diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index 5ddf992f..e9b5c37e 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -17,9 +17,9 @@ from os.path import splitext from cgi import FieldStorage -from webob import Response, exc from werkzeug.utils import secure_filename +from mediagoblin.util import render_to_response, redirect from mediagoblin.decorators import require_active_login from mediagoblin.submit import forms as submit_forms, security from mediagoblin.process_media import process_media_initial @@ -78,23 +78,14 @@ def submit_start(request): # queue it for processing process_media_initial.delay(unicode(entry['_id'])) - # redirect - return exc.HTTPFound( - location=request.urlgen("mediagoblin.submit.success")) + return redirect(request, "mediagoblin.submit.success") - # render - template = request.template_env.get_template( - 'mediagoblin/submit/start.html') - return Response( - template.render( - {'request': request, - 'submit_form': submit_form})) + return render_to_response( + request, + 'mediagoblin/submit/start.html', + {'submit_form': submit_form}) def submit_success(request): - # render - template = request.template_env.get_template( - 'mediagoblin/submit/success.html') - return Response( - template.render( - {'request': request})) + return render_to_response( + request, 'mediagoblin/submit/success.html', {}) diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html index 22a57b70..c2e27c15 100644 --- a/mediagoblin/templates/mediagoblin/auth/login.html +++ b/mediagoblin/templates/mediagoblin/auth/login.html @@ -33,7 +33,8 @@ <input type="submit" value="submit" class="button"/> </div> {% if next %} - <input type="hidden" name="next" value="{{ next }}" class="button" /> + <input type="hidden" name="next" value="{{ next }}" class="button" + style="display: none;"/> {% endif %} <p>Don't have an account yet? <a href="{{ request.urlgen('mediagoblin.auth.register') }}">Create one here!</a></p> </div> diff --git a/mediagoblin/templates/mediagoblin/edit/edit.html b/mediagoblin/templates/mediagoblin/edit/edit.html new file mode 100644 index 00000000..295d57eb --- /dev/null +++ b/mediagoblin/templates/mediagoblin/edit/edit.html @@ -0,0 +1,38 @@ +{# +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#} +{% extends "mediagoblin/base.html" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block mediagoblin_content %} + <h1>Edit details for {{ media.title }}</h1> + + <form action="{{ request.urlgen('mediagoblin.edit.edit_media', + user= media.uploader().username, + media= media._id) }}" + method="POST" enctype="multipart/form-data"> + <div class="submit_box form_box"> + {{ wtforms_util.render_divs(form) }} + <div class="form_submit_buttons"> + <input type="submit" value="submit" class="button" /> + </div> + </div> + </form> + <img src="{{ request.app.public_store.file_url( + media['media_files']['thumb']) }}" /> +{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 886962d1..200f13cd 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -20,11 +20,11 @@ {# temporarily, an "image gallery" that isn't one really ;) #} {% if media %} - <img class="media_image" src="{{ request.app.public_store.file_url( - media.media_files.main) }}" /> <h1> {{media.title}} </h1> + <img class="media_image" src="{{ request.app.public_store.file_url( + media.media_files.main) }}" /> <p>{{ media.description }}</p> <p>Uploaded on {{ "%4d-%02d-%02d"|format(media.created.year, @@ -33,6 +33,9 @@ <a href="{{ request.urlgen('mediagoblin.user_pages.user_home', user= media.uploader().username) }}"> {{- media.uploader().username }}</a></p> + <p><a href="{{ request.urlgen('mediagoblin.edit.edit_media', + user= media.uploader().username, + media= media._id) }}">Edit</a></p> {% else %} <p>Sorry, no such media found.<p/> {% endif %} diff --git a/mediagoblin/templates/mediagoblin/utils/pagination.html b/mediagoblin/templates/mediagoblin/utils/pagination.html index 5ca5e09b..62e8af91 100644 --- a/mediagoblin/templates/mediagoblin/utils/pagination.html +++ b/mediagoblin/templates/mediagoblin/utils/pagination.html @@ -32,7 +32,7 @@ <strong>{{ page }}</strong> {% endif %} {% else %} - <span class=ellipsis>…</span> + <span class="ellipsis">…</span> {% endif %} {%- endfor %} diff --git a/mediagoblin/tests/mgoblin_test_app.ini b/mediagoblin/tests/mgoblin_test_app.ini new file mode 100644 index 00000000..abed2615 --- /dev/null +++ b/mediagoblin/tests/mgoblin_test_app.ini @@ -0,0 +1,46 @@ +[DEFAULT] +debug = true + +[composite:main] +use = egg:Paste#urlmap +/ = mediagoblin +/mgoblin_media/ = publicstore_serve +/mgoblin_static/ = mediagoblin_static + +[app:mediagoblin] +use = egg:mediagoblin#app +filter-with = beaker +queuestore_base_dir = %(here)s/test_user_dev/media/queue +publicstore_base_dir = %(here)s/test_user_dev/media/public +publicstore_base_url = /mgoblin_media/ +direct_remote_path = /mgoblin_static/ +email_sender_address = "notice@mediagoblin.example.org" +email_debug_mode = true +db_name = __mediagoblin_tests__ +# Celery shouldn't be set up by the paste app factory as it's set up +# elsewhere +celery_setup_elsewhere = true + +[app:publicstore_serve] +use = egg:Paste#static +document_root = %(here)s/user_dev/media/public + +[app:mediagoblin_static] +use = egg:Paste#static +document_root = %(here)s/mediagoblin/static/ + +[filter:beaker] +use = egg:Beaker#beaker_session +cache_dir = %(here)s/test_user_dev/beaker +beaker.session.key = mediagoblin +# beaker.session.secret = somesupersecret +beaker.session.data_dir = %(here)s/test_user_dev/beaker/sessions/data +beaker.session.lock_dir = %(here)s/test_user_dev/beaker/sessions/lock + +[celery] +celery_always_eager = true + +[server:main] +use = egg:Paste#http +host = 127.0.0.1 +port = 6543 diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index 94ce6bba..cdfeccab 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -14,8 +14,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import urlparse + +from nose.tools import assert_equal from mediagoblin.auth import lib as auth_lib +from mediagoblin.tests.tools import setup_fresh_app +from mediagoblin import globals as mgoblin_globals +from mediagoblin import util ######################## @@ -57,3 +63,169 @@ def test_bcrypt_gen_password_hash(): pw, hashed_pw, '3><7R45417') assert not auth_lib.bcrypt_check_password( 'notthepassword', hashed_pw, '3><7R45417') + + +@setup_fresh_app +def test_register_views(test_app): + """ + Massive test function that all our registration-related views all work. + """ + # Test doing a simple GET on the page + # ----------------------------------- + + test_app.get('/auth/register/') + # Make sure it rendered with the appropriate template + assert util.TEMPLATE_TEST_CONTEXT.has_key( + 'mediagoblin/auth/register.html') + + # Try to register without providing anything, should error + # -------------------------------------------------------- + + util.clear_test_template_context() + test_app.post( + '/auth/register/', {}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + form = context['register_form'] + assert form.username.errors == [u'This field is required.'] + assert form.password.errors == [u'This field is required.'] + assert form.confirm_password.errors == [u'This field is required.'] + assert form.email.errors == [u'This field is required.'] + + # Try to register with fields that are known to be invalid + # -------------------------------------------------------- + + ## too short + util.clear_test_template_context() + test_app.post( + '/auth/register/', { + 'username': 'l', + 'password': 'o', + 'confirm_password': 'o', + 'email': 'l'}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + form = context['register_form'] + + assert form.username.errors == [ + u'Field must be between 3 and 30 characters long.'] + assert form.password.errors == [ + u'Field must be between 6 and 30 characters long.'] + + ## bad form + util.clear_test_template_context() + test_app.post( + '/auth/register/', { + 'username': '@_@', + 'email': 'lollerskates'}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + form = context['register_form'] + + assert form.username.errors == [ + u'Invalid input.'] + assert form.email.errors == [ + u'Invalid email address.'] + + ## mismatching passwords + util.clear_test_template_context() + test_app.post( + '/auth/register/', { + 'password': 'herpderp', + 'confirm_password': 'derpherp'}) + context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + form = context['register_form'] + + assert form.password.errors == [ + u'Passwords must match.'] + + ## At this point there should be no users in the database ;) + assert not mgoblin_globals.database.User.find().count() + + # Successful register + # ------------------- + util.clear_test_template_context() + response = test_app.post( + '/auth/register/', { + 'username': 'happygirl', + 'password': 'iamsohappy', + 'confirm_password': 'iamsohappy', + 'email': 'happygrrl@example.org'}) + response.follow() + + ## Did we redirect to the proper page? Use the right template? + assert_equal( + urlparse.urlsplit(response.location)[2], + '/auth/register/success/') + assert util.TEMPLATE_TEST_CONTEXT.has_key( + 'mediagoblin/auth/register_success.html') + + ## Make sure user is in place + new_user = mgoblin_globals.database.User.find_one( + {'username': 'happygirl'}) + assert new_user + assert new_user['status'] == u'needs_email_verification' + assert new_user['email_verified'] == False + + ## Make sure we get email confirmation, and try verifying + assert len(util.EMAIL_TEST_INBOX) == 1 + message = util.EMAIL_TEST_INBOX.pop() + assert message['To'] == 'happygrrl@example.org' + email_context = util.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/auth/verification_email.txt'] + assert email_context['verification_url'] in message.get_payload(decode=True) + + path = urlparse.urlsplit(email_context['verification_url'])[2] + get_params = urlparse.urlsplit(email_context['verification_url'])[3] + assert path == u'/auth/verify_email/' + parsed_get_params = urlparse.parse_qs(get_params) + + ### user should have these same parameters + assert parsed_get_params['userid'] == [ + unicode(new_user['_id'])] + assert parsed_get_params['token'] == [ + new_user['verification_key']] + + ## Try verifying with bs verification key, shouldn't work + util.clear_test_template_context() + test_app.get( + "/auth/verify_email/?userid=%s&token=total_bs" % unicode( + new_user['_id'])) + context = util.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/auth/verify_email.html'] + assert context['verification_successful'] == False + new_user = mgoblin_globals.database.User.find_one( + {'username': 'happygirl'}) + assert new_user + assert new_user['status'] == u'needs_email_verification' + assert new_user['email_verified'] == False + + ## Verify the email activation works + util.clear_test_template_context() + test_app.get("%s?%s" % (path, get_params)) + context = util.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/auth/verify_email.html'] + assert context['verification_successful'] == True + new_user = mgoblin_globals.database.User.find_one( + {'username': 'happygirl'}) + assert new_user + assert new_user['status'] == u'active' + assert new_user['email_verified'] == True + + ## TODO: Try logging in + + # Uniqueness checks + # ----------------- + ## We shouldn't be able to register with that user twice + util.clear_test_template_context() + response = test_app.post( + '/auth/register/', { + 'username': 'happygirl', + 'password': 'iamsohappy2', + 'confirm_password': 'iamsohappy2', + 'email': 'happygrrl2@example.org'}) + + context = util.TEMPLATE_TEST_CONTEXT[ + 'mediagoblin/auth/register.html'] + form = context['register_form'] + assert form.username.errors == [ + u'Sorry, a user with that name already exists.'] + + ## TODO: Also check for double instances of an email address? diff --git a/mediagoblin/tests/test_tests.py b/mediagoblin/tests/test_tests.py new file mode 100644 index 00000000..3ecbfac7 --- /dev/null +++ b/mediagoblin/tests/test_tests.py @@ -0,0 +1,38 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin.tests.tools import get_test_app + +from mediagoblin import globals as mgoblin_globals + + +def test_get_test_app_wipes_db(): + """ + Make sure we get a fresh database on every wipe :) + """ + get_test_app() + assert mgoblin_globals.database.User.find().count() == 0 + + new_user = mgoblin_globals.database.User() + new_user['username'] = u'lolcat' + new_user['email'] = u'lol@cats.example.org' + new_user['pw_hash'] = u'pretend_this_is_a_hash' + new_user.save() + assert mgoblin_globals.database.User.find().count() == 1 + + get_test_app() + + assert mgoblin_globals.database.User.find().count() == 0 diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py new file mode 100644 index 00000000..342b54b7 --- /dev/null +++ b/mediagoblin/tests/tools.py @@ -0,0 +1,109 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011 Free Software Foundation, Inc +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import pkg_resources +import os, shutil + +from paste.deploy import appconfig, loadapp +from webtest import TestApp + +from mediagoblin import util +from mediagoblin.decorators import _make_safe +from mediagoblin.db.open import setup_connection_and_db_from_config + + +MEDIAGOBLIN_TEST_DB_NAME = '__mediagoblinunittests__' +TEST_APP_CONFIG = pkg_resources.resource_filename( + 'mediagoblin.tests', 'mgoblin_test_app.ini') +TEST_USER_DEV = pkg_resources.resource_filename( + 'mediagoblin.tests', 'test_user_dev') +MGOBLIN_APP = None + +USER_DEV_DIRECTORIES_TO_SETUP = [ + 'media/public', 'media/queue', + 'beaker/sessions/data', 'beaker/sessions/lock'] + + +class BadCeleryEnviron(Exception): pass + + +def get_test_app(dump_old_app=True): + if not os.environ.get('CELERY_CONFIG_MODULE') == \ + 'mediagoblin.celery_setup.from_tests': + raise BadCeleryEnviron( + u"Sorry, you *absolutely* must run nosetests with the\n" + u"mediagoblin.celery_setup.from_tests module. Like so:\n" + u"$ CELERY_CONFIG_MODULE=mediagoblin.celery_setup.from_tests ./bin/nosetests") + + # Just return the old app if that exists and it's okay to set up + # and return + if MGOBLIN_APP and not dump_old_app: + return MGOBLIN_APP + + # Remove and reinstall user_dev directories + if os.path.exists(TEST_USER_DEV): + shutil.rmtree(TEST_USER_DEV) + + for directory in USER_DEV_DIRECTORIES_TO_SETUP: + full_dir = os.path.join(TEST_USER_DEV, directory) + os.makedirs(full_dir) + + # Get app config + config = appconfig( + 'config:' + os.path.basename(TEST_APP_CONFIG), + relative_to=os.path.dirname(TEST_APP_CONFIG), + name='mediagoblin') + + # Wipe database + # @@: For now we're dropping collections, but we could also just + # collection.remove() ? + connection, db = setup_connection_and_db_from_config( + config.local_conf) + + collections_to_wipe = [ + collection + for collection in db.collection_names() + if not collection.startswith('system.')] + + for collection in collections_to_wipe: + db.drop_collection(collection) + + # Don't need these anymore... + del(connection) + del(db) + + # TODO: Drop and recreate indexes + + # setup app and return + test_app = loadapp( + 'config:' + TEST_APP_CONFIG) + + return TestApp(test_app) + + +def setup_fresh_app(func): + """ + Decorator to setup a fresh test application for this function. + + Cleans out test buckets and passes in a new, fresh test_app. + """ + def wrapper(*args, **kwargs): + test_app = get_test_app() + util.clear_test_buckets() + return func(test_app, *args, **kwargs) + + return _make_safe(wrapper, func) diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py index 96f97427..c5e9a984 100644 --- a/mediagoblin/user_pages/routing.py +++ b/mediagoblin/user_pages/routing.py @@ -22,5 +22,7 @@ user_routes = [ Route('mediagoblin.user_pages.media_home', '/{user}/m/{media}/', requirements=dict(m_id="[0-9a-fA-F]{24}"), controller="mediagoblin.user_pages.views:media_home"), + Route('mediagoblin.edit.edit_media', "/{user}/m/{media}/edit/", + controller="mediagoblin.edit.views:edit_media"), Route('mediagoblin.user_pages.atom_feed', '/{user}/atom/', controller="mediagoblin.user_pages.views:atom_feed")] diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index cc9c7b21..323c3e54 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -14,9 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from webob import Response, exc +from webob import exc from mediagoblin.db.util import DESCENDING -from mediagoblin.util import Pagination +from mediagoblin.util import Pagination, render_to_response from mediagoblin.decorators import uses_pagination, get_user_media_entry @@ -42,26 +42,22 @@ def user_home(request, page): if media_entries == None: return exc.HTTPNotFound() - template = request.template_env.get_template( - 'mediagoblin/user_pages/user.html') - - return Response( - template.render( - {'request': request, - 'user': user, - 'media_entries': media_entries, - 'pagination': pagination})) + return render_to_response( + request, + 'mediagoblin/user_pages/user.html', + {'user': user, + 'media_entries': media_entries, + 'pagination': pagination}) @get_user_media_entry def media_home(request, media): """'Homepage' of a MediaEntry()""" - template = request.template_env.get_template( - 'mediagoblin/user_pages/media.html') - return Response( - template.render( - {'request': request, - 'media': media})) + return render_to_response( + request, + 'mediagoblin/user_pages/media.html', + {'media': media}) + ATOM_DEFAULT_NR_OF_UPDATED_ITEMS = 5 diff --git a/mediagoblin/util.py b/mediagoblin/util.py index 2865cf11..64e21ca9 100644 --- a/mediagoblin/util.py +++ b/mediagoblin/util.py @@ -18,17 +18,22 @@ from email.MIMEText import MIMEText import gettext import pkg_resources import smtplib +import os import sys import re +import urllib +from math import ceil +import copy + +from babel.localedata import exists import jinja2 -from mediagoblin.db.util import ObjectId import translitcodec +from paste.deploy.loadwsgi import NicerConfigParser +from webob import Response, exc from mediagoblin import globals as mgoblin_globals +from mediagoblin.db.util import ObjectId -import urllib -from math import ceil -import copy TESTS_ENABLED = False def _activate_testing(): @@ -39,6 +44,26 @@ def _activate_testing(): TESTS_ENABLED = True +def clear_test_buckets(): + """ + We store some things for testing purposes that should be cleared + when we want a "clean slate" of information for our next round of + tests. Call this function to wipe all that stuff clean. + + Also wipes out some other things we might redefine during testing, + like the jinja envs. + """ + global SETUP_JINJA_ENVS + SETUP_JINJA_ENVS = {} + + global EMAIL_TEST_INBOX + global EMAIL_TEST_MBOX_INBOX + EMAIL_TEST_INBOX = [] + EMAIL_TEST_MBOX_INBOX = [] + + clear_test_template_context() + + def get_jinja_loader(user_template_path=None): """ Set up the Jinja template loaders, possibly allowing for user @@ -55,6 +80,9 @@ def get_jinja_loader(user_template_path=None): return jinja2.PackageLoader('mediagoblin', 'templates') +SETUP_JINJA_ENVS = {} + + def get_jinja_env(template_loader, locale): """ Set up the Jinja environment, @@ -64,6 +92,11 @@ def get_jinja_env(template_loader, locale): """ setup_gettext(locale) + # If we have a jinja environment set up with this locale, just + # return that one. + if SETUP_JINJA_ENVS.has_key(locale): + return SETUP_JINJA_ENVS[locale] + template_env = jinja2.Environment( loader=template_loader, autoescape=True, extensions=['jinja2.ext.i18n']) @@ -72,9 +105,49 @@ def get_jinja_env(template_loader, locale): mgoblin_globals.translations.gettext, mgoblin_globals.translations.ngettext) + if exists(locale): + SETUP_JINJA_ENVS[locale] = template_env + return template_env +# We'll store context information here when doing unit tests +TEMPLATE_TEST_CONTEXT = {} + + +def render_template(request, template_path, context): + """ + Render a template with context. + + Always inserts the request into the context, so you don't have to. + Also stores the context if we're doing unit tests. Helpful! + """ + template = request.template_env.get_template( + template_path) + context['request'] = request + rendered = template.render(context) + + if TESTS_ENABLED: + TEMPLATE_TEST_CONTEXT[template_path] = context + + return rendered + + +def clear_test_template_context(): + global TEMPLATE_TEST_CONTEXT + TEMPLATE_TEST_CONTEXT = {} + + +def render_to_response(request, template, context): + """Much like Django's shortcut.render()""" + return Response(render_template(request, template, context)) + + +def redirect(request, *args, **kwargs): + """Returns a HTTPFound(), takes a request and then urlgen params""" + return exc.HTTPFound(location=request.urlgen(*args, **kwargs)) + + def setup_user_in_request(request): """ Examine a request and tack on a request.user parameter if that's @@ -278,6 +351,30 @@ def get_locale_from_request(request): return locale_to_lower_upper(target_lang) +def read_config_file(conf_file): + """ + Read a paste deploy style config file and process it. + """ + if not os.path.exists(conf_file): + raise IOError( + "MEDIAGOBLIN_CONFIG not set or file does not exist") + + parser = NicerConfigParser(conf_file) + parser.read(conf_file) + parser._defaults.setdefault( + 'here', os.path.dirname(os.path.abspath(conf_file))) + parser._defaults.setdefault( + '__file__', os.path.abspath(conf_file)) + + mgoblin_conf = dict( + [(section_name, dict(parser.items(section_name))) + for section_name in parser.sections()]) + + return mgoblin_conf + + +SETUP_GETTEXTS = {} + def setup_gettext(locale): """ Setup the gettext instance based on this locale @@ -288,8 +385,13 @@ def setup_gettext(locale): # TODO: fallback nicely on translations from pt_PT to pt if not # available, etc. - this_gettext = gettext.translation( - 'mediagoblin', TRANSLATIONS_PATH, [locale], fallback=True) + if SETUP_GETTEXTS.has_key(locale): + this_gettext = SETUP_GETTEXTS[locale] + else: + this_gettext = gettext.translation( + 'mediagoblin', TRANSLATIONS_PATH, [locale], fallback=True) + if exists(locale): + SETUP_GETTEXTS[locale] = this_gettext mgoblin_globals.setup_globals( translations=this_gettext) diff --git a/mediagoblin/views.py b/mediagoblin/views.py index dd722c63..5b6d9773 100644 --- a/mediagoblin/views.py +++ b/mediagoblin/views.py @@ -14,16 +14,23 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from webob import Response +from mediagoblin.util import render_to_response from mediagoblin.db.util import DESCENDING def root_view(request): media_entries = request.db.MediaEntry.find( {u'state': u'processed'}).sort('created', DESCENDING) - template = request.template_env.get_template( - 'mediagoblin/root.html') - return Response( - template.render( - {'request': request, - 'media_entries': media_entries})) + return render_to_response( + request, 'mediagoblin/root.html', + {'media_entries': media_entries}) + + +def simple_template_render(request): + """ + A view for absolutely simple template rendering. + Just make sure 'template' is in the matchdict! + """ + template_name = request.matchdict['template'] + return render_to_response( + request, template_name, {}) @@ -41,6 +41,7 @@ setup( 'Babel', 'translitcodec', 'argparse', + 'webtest', ], test_suite='nose.collector', |