diff options
32 files changed, 373 insertions, 251 deletions
diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py index 0b2bf959..7cae951a 100644 --- a/mediagoblin/auth/forms.py +++ b/mediagoblin/auth/forms.py @@ -17,52 +17,75 @@ import wtforms import re +from mediagoblin.tools.mail import normalize_email from mediagoblin.tools.translate import fake_ugettext_passthrough as _ +def normalize_user_or_email_field(allow_email=True, allow_user=True): + """Check if we were passed a field that matches a username and/or email pattern + + This is useful for fields that can take either a username or email + address. Use the parameters if you want to only allow a username for + instance""" + message = _(u'Invalid User name or email address.') + nomail_msg = _(u"This field does not take email addresses.") + nouser_msg = _(u"This field requires an email address.") + + def _normalize_field(form, field): + email = u'@' in field.data + if email: # normalize email address casing + if not allow_email: + raise wtforms.ValidationError(nomail_msg) + wtforms.validators.Email()(form, field) + field.data = normalize_email(field.data) + else: # lower case user names + if not allow_user: + raise wtforms.ValidationError(nouser_msg) + wtforms.validators.Length(min=3, max=30)(form, field) + wtforms.validators.Regexp(r'^\w+$')(form, field) + field.data = field.data.lower() + if field.data is None: # should not happen, but be cautious anyway + raise wtforms.ValidationError(message) + return _normalize_field + class RegistrationForm(wtforms.Form): username = wtforms.TextField( _('Username'), [wtforms.validators.Required(), - wtforms.validators.Length(min=3, max=30), - wtforms.validators.Regexp(r'^\w+$')]) + normalize_user_or_email_field(allow_email=False)]) password = wtforms.PasswordField( _('Password'), [wtforms.validators.Required(), - wtforms.validators.Length(min=6, max=30)]) + wtforms.validators.Length(min=5, max=1024)]) email = wtforms.TextField( _('Email address'), [wtforms.validators.Required(), - wtforms.validators.Email()]) + normalize_user_or_email_field(allow_user=False)]) class LoginForm(wtforms.Form): username = wtforms.TextField( _('Username'), [wtforms.validators.Required(), - wtforms.validators.Regexp(r'^\w+$')]) + normalize_user_or_email_field(allow_email=False)]) password = wtforms.PasswordField( _('Password'), - [wtforms.validators.Required()]) + [wtforms.validators.Required(), + wtforms.validators.Length(min=5, max=1024)]) class ForgotPassForm(wtforms.Form): username = wtforms.TextField( _('Username or email'), - [wtforms.validators.Required()]) - - def validate_username(form, field): - if not (re.match(r'^\w+$', field.data) or - re.match(r'^.+@[^.].*\.[a-z]{2,10}$', field.data, - re.IGNORECASE)): - raise wtforms.ValidationError(_(u'Incorrect input')) + [wtforms.validators.Required(), + normalize_user_or_email_field()]) class ChangePassForm(wtforms.Form): password = wtforms.PasswordField( 'Password', [wtforms.validators.Required(), - wtforms.validators.Length(min=6, max=30)]) + wtforms.validators.Length(min=5, max=1024)]) userid = wtforms.HiddenField( '', [wtforms.validators.Required()]) diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 43354135..d8ad7b51 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -41,8 +41,10 @@ def email_debug_message(request): def register(request): - """ - Your classic registration view! + """The registration view. + + Note that usernames will always be lowercased. Email domains are lowercased while + the first part remains case-sensitive. """ # Redirects to indexpage if registrations are disabled if not mg_globals.app_config["allow_registration"]: @@ -56,12 +58,8 @@ def register(request): if request.method == 'POST' and register_form.validate(): # TODO: Make sure the user doesn't exist already - username = unicode(request.form['username'].lower()) - em_user, em_dom = unicode(request.form['email']).split("@", 1) - em_dom = em_dom.lower() - email = em_user + "@" + em_dom - users_with_username = User.query.filter_by(username=username).count() - users_with_email = User.query.filter_by(email=email).count() + users_with_username = User.query.filter_by(username=register_form.data['username']).count() + users_with_email = User.query.filter_by(email=register_form.data['email']).count() extra_validation_passes = True @@ -77,8 +75,8 @@ def register(request): if extra_validation_passes: # Create the user user = User() - user.username = username - user.email = email + user.username = register_form.data['username'] + user.email = register_form.data['email'] user.pw_hash = auth_lib.bcrypt_gen_password_hash( request.form['password']) user.verification_key = unicode(uuid.uuid4()) @@ -114,20 +112,21 @@ def login(request): login_failed = False - if request.method == 'POST' and login_form.validate(): - user = User.query.filter_by(username=request.form['username'].lower()).first() + if request.method == 'POST': + if login_form.validate(): + user = User.query.filter_by(username=login_form.data['username']).first() - if user and user.check_login(request.form['password']): - # set up login in session - request.session['user_id'] = unicode(user.id) - request.session.save() + if user and user.check_login(request.form['password']): + # set up login in session + request.session['user_id'] = unicode(user.id) + request.session.save() - if request.form.get('next'): - return redirect(request, location=request.form['next']) - else: - return redirect(request, "index") + if request.form.get('next'): + return redirect(request, location=request.form['next']) + else: + return redirect(request, "index") - else: + # Some failure during login occured if we are here! # Prevent detecting who's on this system by testing login # attempt timings auth_lib.fake_login_attempt() @@ -227,59 +226,66 @@ def forgot_password(request): """ Forgot password view - Sends an email with an url to renew forgotten password + Sends an email with an url to renew forgotten password. + Use GET querystring parameter 'username' to pre-populate the input field """ fp_form = auth_forms.ForgotPassForm(request.form, - username=request.GET.get('username')) - - if request.method == 'POST' and fp_form.validate(): - - # '$or' not available till mongodb 1.5.3 - user = User.query.filter_by(username=request.form['username']).first() - if not user: - user = User.query.filter_by(email=request.form['username']).first() - - if user: - if user.email_verified and user.status == 'active': - user.fp_verification_key = unicode(uuid.uuid4()) - user.fp_token_expire = datetime.datetime.now() + \ - datetime.timedelta(days=10) - user.save() - - send_fp_verification_email(user, request) - - messages.add_message( - request, - messages.INFO, - _("An email has been sent with instructions on how to " - "change your password.")) - email_debug_message(request) - - else: - # special case... we can't send the email because the - # username is inactive / hasn't verified their email - messages.add_message( - request, - messages.WARNING, - _("Could not send password recovery email as " - "your username is inactive or your account's " - "email address has not been verified.")) - - return redirect( - request, 'mediagoblin.user_pages.user_home', - user=user.username) - return redirect(request, 'mediagoblin.auth.login') - else: - messages.add_message( - request, - messages.WARNING, - _("Couldn't find someone with that username or email.")) + username=request.args.get('username')) + + if not (request.method == 'POST' and fp_form.validate()): + # Either GET request, or invalid form submitted. Display the template + return render_to_response(request, + 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form}) + + # If we are here: method == POST and form is valid. username casing + # has been sanitized. Store if a user was found by email. We should + # not reveal if the operation was successful then as we don't want to + # leak if an email address exists in the system. + found_by_email = '@' in request.form['username'] + + if found_by_email: + user = User.query.filter_by( + email = request.form['username']).first() + # Don't reveal success in case the lookup happened by email address. + success_message=_("If that email address (case sensitive!) is " + "registered an email has been sent with instructions " + "on how to change your password.") + + else: # found by username + user = User.query.filter_by( + username = request.form['username']).first() + + if user is None: + messages.add_message(request, + messages.WARNING, + _("Couldn't find someone with that username.")) return redirect(request, 'mediagoblin.auth.forgot_password') - return render_to_response( - request, - 'mediagoblin/auth/forgot_password.html', - {'fp_form': fp_form}) + success_message=_("An email has been sent with instructions " + "on how to change your password.") + + if user and not(user.email_verified and user.status == 'active'): + # Don't send reminder because user is inactive or has no verified email + messages.add_message(request, + messages.WARNING, + _("Could not send password recovery email as your username is in" + "active or your account's email address has not been verified.")) + + return redirect(request, 'mediagoblin.user_pages.user_home', + user=user.username) + + # SUCCESS. Send reminder and return to login page + if user: + user.fp_verification_key = unicode(uuid.uuid4()) + user.fp_token_expire = datetime.datetime.now() + \ + datetime.timedelta(days=10) + user.save() + + email_debug_message(request) + send_fp_verification_email(user, request) + + messages.add_message(request, messages.INFO, success_message) + return redirect(request, 'mediagoblin.auth.login') def verify_forgot_password(request): diff --git a/mediagoblin/db/open.py b/mediagoblin/db/open.py index d976acd8..5fd5ed03 100644 --- a/mediagoblin/db/open.py +++ b/mediagoblin/db/open.py @@ -15,7 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -from sqlalchemy import create_engine +from sqlalchemy import create_engine, event import logging from mediagoblin.db.base import Base, Session @@ -66,9 +66,20 @@ def load_models(app_config): exc)) +def _sqlite_fk_pragma_on_connect(dbapi_con, con_record): + """Enable foreign key checking on each new sqlite connection""" + dbapi_con.execute('pragma foreign_keys=on') + + def setup_connection_and_db_from_config(app_config): engine = create_engine(app_config['sql_engine']) + + # Enable foreign key checking for sqlite + if app_config['sql_engine'].startswith('sqlite://'): + event.listen(engine, 'connect', _sqlite_fk_pragma_on_connect) + # logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + Session.configure(bind=engine) return DatabaseMaster(engine) diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index 88af22d8..24b0b69b 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -33,6 +33,7 @@ from mediagoblin.tools.response import render_to_response, redirect from mediagoblin.tools.translate import pass_to_ugettext as _ from mediagoblin.tools.text import ( convert_to_tag_list_of_dicts, media_tags_as_string) +from mediagoblin.tools.url import slugify from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used import mimetypes @@ -58,22 +59,20 @@ def edit_media(request, media): if request.method == 'POST' and form.validate(): # Make sure there isn't already a MediaEntry with such a slug # and userid. - slug_used = check_media_slug_used(media.uploader, request.form['slug'], - media.id) + slug = slugify(request.form['slug']) + slug_used = check_media_slug_used(media.uploader, slug, media.id) if slug_used: form.slug.errors.append( _(u'An entry with that slug already exists for this user.')) else: - media.title = unicode(request.form['title']) - media.description = unicode(request.form.get('description')) + media.title = request.form['title'] + media.description = request.form.get('description') media.tags = convert_to_tag_list_of_dicts( request.form.get('tags')) media.license = unicode(request.form.get('license', '')) or None - - media.slug = unicode(request.form['slug']) - + media.slug = slug media.save() return redirect(request, diff --git a/mediagoblin/init/__init__.py b/mediagoblin/init/__init__.py index ab6e6399..7c832442 100644 --- a/mediagoblin/init/__init__.py +++ b/mediagoblin/init/__init__.py @@ -26,7 +26,7 @@ from mediagoblin import mg_globals from mediagoblin.mg_globals import setup_globals from mediagoblin.db.open import setup_connection_and_db_from_config, \ check_db_migrations_current, load_models -from mediagoblin.workbench import WorkbenchManager +from mediagoblin.tools.workbench import WorkbenchManager from mediagoblin.storage import storage_system_from_config diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py index 22c4355d..4c9f0131 100644 --- a/mediagoblin/media_types/video/processing.py +++ b/mediagoblin/media_types/video/processing.py @@ -75,9 +75,8 @@ def process_video(entry, workbench=None): thumbnail_filepath = create_pub_filepath( entry, name_builder.fill('{basename}.thumbnail.jpg')) - # Create a temporary file for the video destination - tmp_dst = NamedTemporaryFile(dir=workbench.dir) - + # Create a temporary file for the video destination (cleaned up with workbench) + tmp_dst = NamedTemporaryFile(dir=workbench.dir, delete=False) with tmp_dst: # Transcode queued file to a VP8/vorbis file that fits in a 640x640 square progress_callback = ProgressCallback(entry) @@ -88,22 +87,20 @@ def process_video(entry, workbench=None): vorbis_quality=video_config['vorbis_quality'], progress_callback=progress_callback) - # Push transcoded video to public storage - _log.debug('Saving medium...') - # TODO (#419, we read everything in RAM here!) - mgg.public_store.get_file(medium_filepath, 'wb').write( - tmp_dst.read()) - _log.debug('Saved medium') + # Push transcoded video to public storage + _log.debug('Saving medium...') + mgg.public_store.copy_local_to_storage(tmp_dst.name, medium_filepath) + _log.debug('Saved medium') - entry.media_files['webm_640'] = medium_filepath + entry.media_files['webm_640'] = medium_filepath - # Save the width and height of the transcoded video - entry.media_data_init( - width=transcoder.dst_data.videowidth, - height=transcoder.dst_data.videoheight) + # Save the width and height of the transcoded video + entry.media_data_init( + width=transcoder.dst_data.videowidth, + height=transcoder.dst_data.videoheight) - # Create a temporary file for the video thumbnail - tmp_thumb = NamedTemporaryFile(dir=workbench.dir, suffix='.jpg') + # Temporary file for the video thumbnail (cleaned up with workbench) + tmp_thumb = NamedTemporaryFile(dir=workbench.dir, suffix='.jpg', delete=False) with tmp_thumb: # Create a thumbnail.jpg that fits in a 180x180 square @@ -112,33 +109,16 @@ def process_video(entry, workbench=None): tmp_thumb.name, 180) - # Push the thumbnail to public storage - _log.debug('Saving thumbnail...') - mgg.public_store.get_file(thumbnail_filepath, 'wb').write( - tmp_thumb.read()) - _log.debug('Saved thumbnail') - - entry.media_files['thumb'] = thumbnail_filepath + # Push the thumbnail to public storage + _log.debug('Saving thumbnail...') + mgg.public_store.copy_local_to_storage(tmp_thumb.name, thumbnail_filepath) + entry.media_files['thumb'] = thumbnail_filepath if video_config['keep_original']: # Push original file to public storage - queued_file = file(queued_filename, 'rb') - - with queued_file: - original_filepath = create_pub_filepath( - entry, - queued_filepath[-1]) - - with mgg.public_store.get_file(original_filepath, 'wb') as \ - original_file: - _log.debug('Saving original...') - # TODO (#419, we read everything in RAM here!) - original_file.write(queued_file.read()) - _log.debug('Saved original') - - entry.media_files['original'] = original_filepath + _log.debug('Saving original...') + original_filepath = create_pub_filepath(entry, queued_filepath[-1]) + mgg.public_store.copy_local_to_storage(queued_filename, original_filepath) + entry.media_files['original'] = original_filepath mgg.queue_store.delete_file(queued_filepath) - - # clean up workbench - workbench.destroy_self() diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py index 6aa4ef9f..2055a663 100644 --- a/mediagoblin/plugins/api/views.py +++ b/mediagoblin/plugins/api/views.py @@ -86,7 +86,10 @@ def post_entry(request): # # (... don't change entry after this point to avoid race # conditions with changes to the document via processing code) - run_process_media(entry) + feed_url = request.urlgen( + 'mediagoblin.user_pages.atom_feed', + qualified=True, user=request.user.username) + run_process_media(entry, feed_url) return json_response(get_entry_serializable(entry, request.urlgen)) diff --git a/mediagoblin/processing/task.py b/mediagoblin/processing/task.py index b29de9bd..e9bbe084 100644 --- a/mediagoblin/processing/task.py +++ b/mediagoblin/processing/task.py @@ -15,8 +15,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import logging +import urllib +import urllib2 -from celery.task import Task +from celery import registry, task from mediagoblin import mg_globals as mgg from mediagoblin.db.models import MediaEntry @@ -28,18 +30,51 @@ logging.basicConfig() _log.setLevel(logging.DEBUG) +@task.task(default_retry_delay=2 * 60) +def handle_push_urls(feed_url): + """Subtask, notifying the PuSH servers of new content + + Retry 3 times every 2 minutes if run in separate process before failing.""" + if not mgg.app_config["push_urls"]: + return # Nothing to do + _log.debug('Notifying Push servers for feed {0}'.format(feed_url)) + hubparameters = { + 'hub.mode': 'publish', + 'hub.url': feed_url} + hubdata = urllib.urlencode(hubparameters) + hubheaders = { + "Content-type": "application/x-www-form-urlencoded", + "Connection": "close"} + for huburl in mgg.app_config["push_urls"]: + hubrequest = urllib2.Request(huburl, hubdata, hubheaders) + try: + hubresponse = urllib2.urlopen(hubrequest) + except (urllib2.HTTPError, urllib2.URLError) as exc: + # We retry by default 3 times before failing + _log.info("PuSH url %r gave error %r", huburl, exc) + try: + return handle_push_urls.retry(exc=exc, throw=False) + except Exception as e: + # All retries failed, Failure is no tragedy here, probably. + _log.warn('Failed to notify PuSH server for feed {0}. ' + 'Giving up.'.format(feed_url)) + return False + ################################ # Media processing initial steps ################################ -class ProcessMedia(Task): +class ProcessMedia(task.Task): """ Pass this entry off for processing. """ - def run(self, media_id): + def run(self, media_id, feed_url): """ Pass the media entry off to the appropriate processing function (for now just process_image...) + + :param feed_url: The feed URL that the PuSH server needs to be + updated for. """ entry = MediaEntry.query.get(media_id) @@ -58,6 +93,10 @@ class ProcessMedia(Task): entry.state = u'processed' entry.save() + # Notify the PuSH servers as async task + if mgg.app_config["push_urls"] and feed_url: + handle_push_urls.subtask().delay(feed_url) + json_processing_callback(entry) except BaseProcessingFail as exc: mark_entry_failed(entry.id, exc) @@ -97,3 +136,7 @@ class ProcessMedia(Task): entry = mgg.database.MediaEntry.query.filter_by(id=entry_id).first() json_processing_callback(entry) + +# Register the task +process_media = registry.tasks[ProcessMedia.name] + diff --git a/mediagoblin/submit/lib.py b/mediagoblin/submit/lib.py index db5dfe53..679fc543 100644 --- a/mediagoblin/submit/lib.py +++ b/mediagoblin/submit/lib.py @@ -14,16 +14,12 @@ # 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 urllib -import urllib2 import logging import uuid -from celery import registry from werkzeug.utils import secure_filename -from mediagoblin import mg_globals from mediagoblin.processing import mark_entry_failed -from mediagoblin.processing.task import ProcessMedia +from mediagoblin.processing.task import process_media _log = logging.getLogger(__name__) @@ -58,11 +54,17 @@ def prepare_queue_task(app, entry, filename): return queue_file -def run_process_media(entry): - process_media = registry.tasks[ProcessMedia.name] +def run_process_media(entry, feed_url=None): + """Process the media asynchronously + + :param entry: MediaEntry() instance to be processed. + :param feed_url: A string indicating the feed_url that the PuSH servers + should be notified of. This will be sth like: `request.urlgen( + 'mediagoblin.user_pages.atom_feed',qualified=True, + user=request.user.username)`""" try: process_media.apply_async( - [unicode(entry.id)], {}, + [entry.id, feed_url], {}, task_id=entry.queued_task_id) except BaseException as exc: # The purpose of this section is because when running in "lazy" @@ -76,30 +78,3 @@ def run_process_media(entry): mark_entry_failed(entry.id, exc) # re-raise the exception raise - - -def handle_push_urls(request): - if mg_globals.app_config["push_urls"]: - feed_url = request.urlgen( - 'mediagoblin.user_pages.atom_feed', - qualified=True, - user=request.user.username) - hubparameters = { - 'hub.mode': 'publish', - 'hub.url': feed_url} - hubdata = urllib.urlencode(hubparameters) - hubheaders = { - "Content-type": "application/x-www-form-urlencoded", - "Connection": "close"} - for huburl in mg_globals.app_config["push_urls"]: - hubrequest = urllib2.Request(huburl, hubdata, hubheaders) - try: - hubresponse = urllib2.urlopen(hubrequest) - except urllib2.HTTPError as exc: - # This is not a big issue, the item will be fetched - # by the PuSH server next time we hit it - _log.warning( - "push url %r gave error %r", huburl, exc.code) - except urllib2.URLError as exc: - _log.warning( - "push url %r is unreachable %r", huburl, exc.reason) diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py index 38adf85f..4055d394 100644 --- a/mediagoblin/submit/views.py +++ b/mediagoblin/submit/views.py @@ -32,8 +32,7 @@ from mediagoblin.submit import forms as submit_forms from mediagoblin.messages import add_message, SUCCESS from mediagoblin.media_types import sniff_media, \ InvalidFileType, FileTypeNotSupported -from mediagoblin.submit.lib import handle_push_urls, run_process_media, \ - prepare_queue_task +from mediagoblin.submit.lib import run_process_media, prepare_queue_task @require_active_login @@ -91,10 +90,10 @@ def submit_start(request): # # (... don't change entry after this point to avoid race # conditions with changes to the document via processing code) - run_process_media(entry) - - handle_push_urls(request) - + feed_url = request.urlgen( + 'mediagoblin.user_pages.atom_feed', + qualified=True, user=request.user.username) + run_process_media(entry, feed_url) add_message(request, SUCCESS, _('Woohoo! Submitted!')) return redirect(request, "mediagoblin.user_pages.user_home", diff --git a/mediagoblin/templates/mediagoblin/edit/delete_account.html b/mediagoblin/templates/mediagoblin/edit/delete_account.html index 6d56d77c..84d0b580 100644 --- a/mediagoblin/templates/mediagoblin/edit/delete_account.html +++ b/mediagoblin/templates/mediagoblin/edit/delete_account.html @@ -24,11 +24,16 @@ <form action="{{ request.urlgen('mediagoblin.edit.delete_account') }}" method="POST" enctype="multipart/form-data"> <div class="form_box"> - <h1>Really delete user '{{ user.username }}' and all related media/comments? + <h1> + {%- trans user_name=user.username -%} + Really delete user '{{ user_name }}' and all related media/comments? + {%- endtrans -%} </h1> <p class="delete_checkbox_box"> <input type="checkbox" name="confirmed"/> - <label for="confirmed">Yes, really delete my account</label> + <label for="confirmed"> + {%- trans %}Yes, really delete my account{% endtrans -%} + </label> </p> <div class="form_submit_buttons"> diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html index 89f559af..4b980301 100644 --- a/mediagoblin/templates/mediagoblin/edit/edit_account.html +++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html @@ -58,7 +58,8 @@ </div> </form> <div class="delete"> - <a href="{{request.urlgen('mediagoblin.edit.delete_account') - }}">Delete my account</a> + <a href="{{ request.urlgen('mediagoblin.edit.delete_account') }}"> + {%- trans %}Delete my account{% endtrans -%} + </a> </div> {% endblock %} diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 10b48296..7e184257 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -105,9 +105,6 @@ <form action="{{ request.urlgen('mediagoblin.user_pages.media_post_comment', user= media.get_uploader.username, media_id=media.id) }}" method="POST" id="form_comment"> - <p> - {% trans %}You can use <a href="http://daringfireball.net/projects/markdown/basics">Markdown</a> for formatting.{% endtrans %} - </p> {{ wtforms_util.render_divs(comment_form) }} <div class="form_submit_buttons"> <input type="submit" value="{% trans %}Add this comment{% endtrans %}" class="button_action" /> @@ -115,35 +112,38 @@ </div> </form> {% endif %} + <ul style="list-style:none"> {% for comment in comments %} {% set comment_author = comment.get_author %} - {% if pagination.active_id == comment.id %} - <div class="comment_wrapper comment_active" id="comment-{{ comment.id }}"> - <a name="comment" id="comment"></a> - {% else %} - <div class="comment_wrapper" id="comment-{{ comment.id }}"> - {% endif %} - <div class="comment_author"> + <li id="comment-{{ comment.id }}" + {%- if pagination.active_id == comment.id %} + class="comment_wrapper comment_active"> + <a name="comment" id="comment"></a> + {%- else %} + class="comment_wrapper"> + {%- endif %} + <div class="comment_author"> <img src="{{ request.staticdirect('/images/icon_comment.png') }}" /> <a href="{{ request.urlgen('mediagoblin.user_pages.user_home', user = comment_author.username) }}"> - {{ comment_author.username -}} + {{- comment_author.username -}} </a> {% trans %}at{% endtrans %} <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.view_comment', comment = comment.id, user = media.get_uploader.username, media = media.slug_or_id) }}#comment"> - {{ comment.created.strftime("%I:%M%p %Y-%m-%d") }} + {{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}} </a>: </div> <div class="comment_content"> - {% autoescape False %} + {% autoescape False -%} {{ comment.content_html }} - {% endautoescape %} + {%- endautoescape %} </div> - </div> + </li> {% endfor %} + </ul> {{ render_pagination(request, pagination, media.url_for_self(request.urlgen)) }} {% endif %} diff --git a/mediagoblin/tests/test_api.py b/mediagoblin/tests/test_api.py index 4b784da3..82b1c1b4 100644 --- a/mediagoblin/tests/test_api.py +++ b/mediagoblin/tests/test_api.py @@ -22,7 +22,7 @@ from pkg_resources import resource_filename from mediagoblin import mg_globals from mediagoblin.tools import template, pluginapi -from mediagoblin.tests.tools import get_test_app, fixture_add_user +from mediagoblin.tests.tools import get_app, fixture_add_user _log = logging.getLogger(__name__) @@ -44,7 +44,7 @@ BIG_BLUE = resource('bigblue.png') class TestAPI(object): def setUp(self): - self.app = get_test_app(dump_old_app=False) + self.app = get_app(dump_old_app=False) self.db = mg_globals.database self.user_password = u'4cc355_70k3N' diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py index a40c9cbc..f4409121 100644 --- a/mediagoblin/tests/test_auth.py +++ b/mediagoblin/tests/test_auth.py @@ -22,7 +22,7 @@ from nose.tools import assert_equal from mediagoblin import mg_globals from mediagoblin.auth import lib as auth_lib from mediagoblin.db.models import User -from mediagoblin.tests.tools import get_test_app, fixture_add_user +from mediagoblin.tests.tools import setup_fresh_app, get_app, fixture_add_user from mediagoblin.tools import template, mail @@ -67,11 +67,11 @@ def test_bcrypt_gen_password_hash(): 'notthepassword', hashed_pw, '3><7R45417') -def test_register_views(): +@setup_fresh_app +def test_register_views(test_app): """ Massive test function that all our registration-related views all work. """ - test_app = get_test_app(dump_old_app=False) # Test doing a simple GET on the page # ----------------------------------- @@ -105,10 +105,8 @@ def test_register_views(): context = template.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.'] + assert_equal (form.username.errors, [u'Field must be between 3 and 30 characters long.']) + assert_equal (form.password.errors, [u'Field must be between 5 and 1024 characters long.']) ## bad form template.clear_test_template_context() @@ -119,13 +117,11 @@ def test_register_views(): context = template.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.'] + assert_equal (form.username.errors, [u'This field does not take email addresses.']) + assert_equal (form.email.errors, [u'This field requires an email address.']) ## At this point there should be no users in the database ;) - assert not User.query.count() + assert_equal(User.query.count(), 0) # Successful register # ------------------- @@ -315,7 +311,7 @@ def test_authentication_views(): """ Test logging in and logging out """ - test_app = get_test_app(dump_old_app=False) + test_app = get_app(dump_old_app=False) # Make a new user test_user = fixture_add_user(active_user=False) @@ -370,7 +366,7 @@ def test_authentication_views(): response = test_app.post( '/auth/login/', { 'username': u'chris', - 'password': 'jam'}) + 'password': 'jam_and_ham'}) context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] assert context['login_failed'] diff --git a/mediagoblin/tests/test_collections.py b/mediagoblin/tests/test_collections.py new file mode 100644 index 00000000..b19f6362 --- /dev/null +++ b/mediagoblin/tests/test_collections.py @@ -0,0 +1,37 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2013 MediaGoblin contributors. See AUTHORS. +# +# 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 fixture_add_collection, fixture_add_user, \ + get_app +from mediagoblin.db.models import Collection, User +from mediagoblin.db.base import Session +from nose.tools import assert_equal + + +def test_user_deletes_collection(): + # Setup db. + get_app(dump_old_app=False) + + user = fixture_add_user() + coll = fixture_add_collection(user=user) + # Reload into session: + user = User.query.get(user.id) + + cnt1 = Collection.query.count() + user.delete() + cnt2 = Collection.query.count() + + assert_equal(cnt1, cnt2 + 1) diff --git a/mediagoblin/tests/test_csrf_middleware.py b/mediagoblin/tests/test_csrf_middleware.py index 22a0eb04..e720264c 100644 --- a/mediagoblin/tests/test_csrf_middleware.py +++ b/mediagoblin/tests/test_csrf_middleware.py @@ -14,12 +14,12 @@ # 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.tests.tools import get_app from mediagoblin import mg_globals def test_csrf_cookie_set(): - test_app = get_test_app(dump_old_app=False) + test_app = get_app(dump_old_app=False) cookie_name = mg_globals.app_config['csrf_cookie_name'] # get login page @@ -37,7 +37,7 @@ def test_csrf_token_must_match(): # We need a fresh app for this test on webtest < 1.3.6. # We do not understand why, but it fixes the tests. # If we require webtest >= 1.3.6, we can switch to a non fresh app here. - test_app = get_test_app(dump_old_app=True) + test_app = get_app(dump_old_app=True) # construct a request with no cookie or form token assert test_app.post('/auth/login/', @@ -68,7 +68,7 @@ def test_csrf_token_must_match(): status_int == 200 def test_csrf_exempt(): - test_app = get_test_app(dump_old_app=False) + test_app = get_app(dump_old_app=False) # monkey with the views to decorate a known endpoint import mediagoblin.auth.views from mediagoblin.meddleware.csrf import csrf_exempt diff --git a/mediagoblin/tests/test_edit.py b/mediagoblin/tests/test_edit.py index 4bea9243..7db6eaea 100644 --- a/mediagoblin/tests/test_edit.py +++ b/mediagoblin/tests/test_edit.py @@ -18,13 +18,13 @@ from nose.tools import assert_equal from mediagoblin import mg_globals from mediagoblin.db.models import User -from mediagoblin.tests.tools import get_test_app, fixture_add_user +from mediagoblin.tests.tools import get_app, fixture_add_user from mediagoblin.tools import template from mediagoblin.auth.lib import bcrypt_check_password class TestUserEdit(object): def setUp(self): - self.app = get_test_app(dump_old_app=False) + self.app = get_app(dump_old_app=False) # set up new user self.user_password = u'toast' self.user = fixture_add_user(password = self.user_password) diff --git a/mediagoblin/tests/test_http_callback.py b/mediagoblin/tests/test_http_callback.py index 0f6e489f..8bee7045 100644 --- a/mediagoblin/tests/test_http_callback.py +++ b/mediagoblin/tests/test_http_callback.py @@ -20,14 +20,14 @@ from urlparse import urlparse, parse_qs from mediagoblin import mg_globals from mediagoblin.tools import processing -from mediagoblin.tests.tools import get_test_app, fixture_add_user +from mediagoblin.tests.tools import get_app, fixture_add_user from mediagoblin.tests.test_submission import GOOD_PNG from mediagoblin.tests import test_oauth as oauth class TestHTTPCallback(object): def setUp(self): - self.app = get_test_app(dump_old_app=False) + self.app = get_app(dump_old_app=False) self.db = mg_globals.database self.user_password = u'secret' diff --git a/mediagoblin/tests/test_messages.py b/mediagoblin/tests/test_messages.py index c587e599..4c0f3e2e 100644 --- a/mediagoblin/tests/test_messages.py +++ b/mediagoblin/tests/test_messages.py @@ -15,7 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from mediagoblin.messages import fetch_messages, add_message -from mediagoblin.tests.tools import get_test_app +from mediagoblin.tests.tools import get_app from mediagoblin.tools import template @@ -26,7 +26,7 @@ def test_messages(): fetched messages should be the same as the added ones, and fetching should clear the message list. """ - test_app = get_test_app(dump_old_app=False) + test_app = get_app(dump_old_app=False) # Aquire a request object test_app.get('/') context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] diff --git a/mediagoblin/tests/test_misc.py b/mediagoblin/tests/test_misc.py index 8a96e7d0..ae5d7e50 100644 --- a/mediagoblin/tests/test_misc.py +++ b/mediagoblin/tests/test_misc.py @@ -16,9 +16,9 @@ from nose.tools import assert_equal -from mediagoblin.tests.tools import get_test_app +from mediagoblin.tests.tools import get_app def test_404_for_non_existent(): - test_app = get_test_app(dump_old_app=False) + test_app = get_app(dump_old_app=False) res = test_app.get('/does-not-exist/', expect_errors=True) assert_equal(res.status_int, 404) diff --git a/mediagoblin/tests/test_oauth.py b/mediagoblin/tests/test_oauth.py index a72f766e..94ba5dab 100644 --- a/mediagoblin/tests/test_oauth.py +++ b/mediagoblin/tests/test_oauth.py @@ -21,7 +21,7 @@ from urlparse import parse_qs, urlparse from mediagoblin import mg_globals from mediagoblin.tools import template, pluginapi -from mediagoblin.tests.tools import get_test_app, fixture_add_user +from mediagoblin.tests.tools import get_app, fixture_add_user _log = logging.getLogger(__name__) @@ -29,7 +29,7 @@ _log = logging.getLogger(__name__) class TestOAuth(object): def setUp(self): - self.app = get_test_app() + self.app = get_app() self.db = mg_globals.database self.pman = pluginapi.PluginManager() diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index 53330c48..00f1ed3d 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -24,7 +24,7 @@ import os from nose.tools import assert_equal, assert_true from pkg_resources import resource_filename -from mediagoblin.tests.tools import get_test_app, \ +from mediagoblin.tests.tools import get_app, \ fixture_add_user from mediagoblin import mg_globals from mediagoblin.tools import template @@ -50,7 +50,7 @@ REQUEST_CONTEXT = ['mediagoblin/user_pages/user.html', 'request'] class TestSubmission: def setUp(self): - self.test_app = get_test_app(dump_old_app=False) + self.test_app = get_app(dump_old_app=False) # TODO: Possibly abstract into a decorator like: # @as_authenticated_user('chris') @@ -161,11 +161,17 @@ class TestSubmission: media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) media_id = media.id - # At least render the edit page + # render and post to the edit page. edit_url = request.urlgen( 'mediagoblin.edit.edit_media', user=self.test_user.username, media_id=media_id) self.test_app.get(edit_url) + self.test_app.post(edit_url, + {'title': u'Balanced Goblin', + 'slug': u"Balanced=Goblin", + 'tags': u''}) + media = self.check_media(request, {'title': u'Balanced Goblin'}, 1) + assert_equal(media.slug, u"balanced-goblin") # Add a comment, so we can test for its deletion later. self.check_comments(request, media_id, 0) diff --git a/mediagoblin/tests/test_tags.py b/mediagoblin/tests/test_tags.py index 73af2eea..ccb93085 100644 --- a/mediagoblin/tests/test_tags.py +++ b/mediagoblin/tests/test_tags.py @@ -14,7 +14,7 @@ # 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.tests.tools import get_app from mediagoblin.tools import text def test_list_of_dicts_conversion(): @@ -24,7 +24,7 @@ def test_list_of_dicts_conversion(): as a dict. Each tag dict should contain the tag's name and slug. Another function performs the reverse operation when populating a form to edit tags. """ - test_app = get_test_app(dump_old_app=False) + test_app = get_app(dump_old_app=False) # Leading, trailing, and internal whitespace should be removed and slugified assert text.convert_to_tag_list_of_dicts('sleep , 6 AM, chainsaw! ') == [ {'name': u'sleep', 'slug': u'sleep'}, diff --git a/mediagoblin/tests/test_tests.py b/mediagoblin/tests/test_tests.py index d09e8f28..d539f1e0 100644 --- a/mediagoblin/tests/test_tests.py +++ b/mediagoblin/tests/test_tests.py @@ -15,22 +15,22 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from mediagoblin import mg_globals -from mediagoblin.tests.tools import get_test_app, fixture_add_user +from mediagoblin.tests.tools import get_app, fixture_add_user from mediagoblin.db.models import User -def test_get_test_app_wipes_db(): +def test_get_app_wipes_db(): """ Make sure we get a fresh database on every wipe :) """ - get_test_app(dump_old_app=True) + get_app(dump_old_app=True) assert User.query.count() == 0 fixture_add_user() assert User.query.count() == 1 - get_test_app(dump_old_app=False) + get_app(dump_old_app=False) assert User.query.count() == 1 - get_test_app(dump_old_app=True) + get_app(dump_old_app=True) assert User.query.count() == 0 diff --git a/mediagoblin/tests/test_workbench.py b/mediagoblin/tests/test_workbench.py index 9da8eea0..636c8689 100644 --- a/mediagoblin/tests/test_workbench.py +++ b/mediagoblin/tests/test_workbench.py @@ -18,7 +18,7 @@ import os import tempfile -from mediagoblin import workbench +from mediagoblin.tools import workbench from mediagoblin.mg_globals import setup_globals from mediagoblin.decorators import get_workbench from mediagoblin.tests.test_storage import get_tmp_filestorage diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py index 3e78b2e3..18d4ec0c 100644 --- a/mediagoblin/tests/tools.py +++ b/mediagoblin/tests/tools.py @@ -25,7 +25,7 @@ from paste.deploy import loadapp from webtest import TestApp from mediagoblin import mg_globals -from mediagoblin.db.models import User +from mediagoblin.db.models import User, Collection from mediagoblin.tools import testing from mediagoblin.init.config import read_mediagoblin_config from mediagoblin.db.open import setup_connection_and_db_from_config @@ -103,7 +103,7 @@ def suicide_if_bad_celery_environ(): raise BadCeleryEnviron(BAD_CELERY_MESSAGE) -def get_test_app(dump_old_app=True): +def get_app(dump_old_app=True): suicide_if_bad_celery_environ() # Make sure we've turned on testing @@ -164,7 +164,7 @@ def setup_fresh_app(func): """ @wraps(func) def wrapper(*args, **kwargs): - test_app = get_test_app() + test_app = get_app() testing.clear_test_buckets() return func(test_app, *args, **kwargs) @@ -226,3 +226,24 @@ def fixture_add_user(username=u'chris', password=u'toast', Session.expunge(test_user) return test_user + + +def fixture_add_collection(name=u"My first Collection", user=None): + if user is None: + user = fixture_add_user() + coll = Collection.query.filter_by(creator=user.id, title=name).first() + if coll is not None: + return coll + coll = Collection() + coll.creator = user.id + coll.title = name + coll.generate_slug() + coll.save() + + # Reload + Session.refresh(coll) + + # ... and detach from session: + Session.expunge(coll) + + return coll diff --git a/mediagoblin/tools/mail.py b/mediagoblin/tools/mail.py index 8639ba0c..4fa02ce5 100644 --- a/mediagoblin/tools/mail.py +++ b/mediagoblin/tools/mail.py @@ -122,3 +122,16 @@ def send_email(from_addr, to_addrs, subject, message_body): print message.get_payload(decode=True) return mhost.sendmail(from_addr, to_addrs, message.as_string()) + + +def normalize_email(email): + """return case sensitive part, lower case domain name + + :returns: None in case of broken email addresses""" + try: + em_user, em_dom = email.split('@', 1) + except ValueError: + # email contained no '@' + return None + email = "@".join((em_user, em_dom.lower())) + return email diff --git a/mediagoblin/workbench.py b/mediagoblin/tools/workbench.py index 0d4db52b..0bd4096b 100644 --- a/mediagoblin/workbench.py +++ b/mediagoblin/tools/workbench.py @@ -19,10 +19,6 @@ import shutil import tempfile -DEFAULT_WORKBENCH_DIR = os.path.join( - tempfile.gettempdir(), u'mgoblin_workbench') - - # Actual workbench stuff # ---------------------- diff --git a/mediagoblin/user_pages/forms.py b/mediagoblin/user_pages/forms.py index 9e8ccf01..c7398d84 100644 --- a/mediagoblin/user_pages/forms.py +++ b/mediagoblin/user_pages/forms.py @@ -20,8 +20,11 @@ from mediagoblin.tools.translate import fake_ugettext_passthrough as _ class MediaCommentForm(wtforms.Form): comment_content = wtforms.TextAreaField( - '', - [wtforms.validators.Required()]) + _('Comment'), + [wtforms.validators.Required()], + description=_(u'You can use ' + u'<a href="http://daringfireball.net/projects/markdown/basics">' + u'Markdown</a> for formatting.')) class ConfirmDeleteForm(wtforms.Form): confirm = wtforms.BooleanField( diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py index a9431405..2b228355 100644 --- a/mediagoblin/user_pages/routing.py +++ b/mediagoblin/user_pages/routing.py @@ -37,7 +37,7 @@ add_route('mediagoblin.user_pages.user_gallery', 'mediagoblin.user_pages.views:user_gallery') add_route('mediagoblin.user_pages.media_home.view_comment', - '/u/<string:user>/m/<string:media>/c/<string:comment>/', + '/u/<string:user>/m/<string:media>/c/<int:comment>/', 'mediagoblin.user_pages.views:media_home') add_route('mediagoblin.user_pages.atom_feed', diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 30c78a38..dea47fbf 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -110,12 +110,13 @@ def media_home(request, media, page, **kwargs): """ 'Homepage' of a MediaEntry() """ - if request.matchdict.get('comment', None): + comment_id = request.matchdict.get('comment', None) + if comment_id: pagination = Pagination( page, media.get_comments( mg_globals.app_config['comments_ascending']), MEDIA_COMMENTS_PER_PAGE, - request.matchdict.get('comment')) + comment_id) else: pagination = Pagination( page, media.get_comments( @@ -226,6 +227,10 @@ def media_collect(request, media): messages.add_message( request, messages.ERROR, _('You have to select or add a collection')) + return redirect(request, "mediagoblin.user_pages.media_collect", + user=media.get_uploader.username, + media=media.id) + # Check whether media already exists in collection elif CollectionItem.query.filter_by( |