aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mediagoblin/auth/forms.py51
-rw-r--r--mediagoblin/auth/views.py146
-rw-r--r--mediagoblin/config_spec.ini4
-rw-r--r--mediagoblin/db/migrations.py11
-rw-r--r--mediagoblin/db/models.py77
-rw-r--r--mediagoblin/db/open.py13
-rw-r--r--mediagoblin/decorators.py24
-rw-r--r--mediagoblin/edit/forms.py13
-rw-r--r--mediagoblin/edit/routing.py2
-rw-r--r--mediagoblin/edit/views.py105
-rw-r--r--mediagoblin/init/__init__.py2
-rw-r--r--mediagoblin/media_types/ascii/processing.py13
-rw-r--r--mediagoblin/media_types/audio/processing.py24
-rw-r--r--mediagoblin/media_types/image/processing.py33
-rw-r--r--mediagoblin/media_types/stl/processing.py16
-rw-r--r--mediagoblin/media_types/video/processing.py69
-rw-r--r--mediagoblin/plugins/api/views.py5
-rw-r--r--mediagoblin/processing/task.py49
-rw-r--r--mediagoblin/static/css/base.css29
-rw-r--r--mediagoblin/static/js/header_dropdown.js27
-rw-r--r--mediagoblin/submit/lib.py45
-rw-r--r--mediagoblin/submit/views.py14
-rw-r--r--mediagoblin/templates/mediagoblin/base.html52
-rw-r--r--mediagoblin/templates/mediagoblin/edit/delete_account.html48
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit.html2
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit_account.html9
-rw-r--r--mediagoblin/templates/mediagoblin/root.html27
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/collection_list.html56
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html36
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html2
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/user.html6
-rw-r--r--mediagoblin/tests/test_api.py4
-rw-r--r--mediagoblin/tests/test_auth.py24
-rw-r--r--mediagoblin/tests/test_collections.py37
-rw-r--r--mediagoblin/tests/test_csrf_middleware.py8
-rw-r--r--mediagoblin/tests/test_edit.py22
-rw-r--r--mediagoblin/tests/test_http_callback.py4
-rw-r--r--mediagoblin/tests/test_messages.py4
-rw-r--r--mediagoblin/tests/test_misc.py4
-rw-r--r--mediagoblin/tests/test_oauth.py4
-rw-r--r--mediagoblin/tests/test_submission.py20
-rw-r--r--mediagoblin/tests/test_tags.py4
-rw-r--r--mediagoblin/tests/test_tests.py10
-rw-r--r--mediagoblin/tests/test_workbench.py36
-rw-r--r--mediagoblin/tests/tools.py27
-rw-r--r--mediagoblin/tools/mail.py13
-rw-r--r--mediagoblin/tools/workbench.py (renamed from mediagoblin/workbench.py)31
-rw-r--r--mediagoblin/user_pages/forms.py7
-rw-r--r--mediagoblin/user_pages/routing.py12
-rw-r--r--mediagoblin/user_pages/views.py44
50 files changed, 918 insertions, 407 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/config_spec.ini b/mediagoblin/config_spec.ini
index bee67d46..50ce252e 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -86,6 +86,10 @@ max_height = integer(default=640)
max_width = integer(default=180)
max_height = integer(default=180)
+[media_type:mediagoblin.media_types.image]
+# One of BICUBIC, BILINEAR, NEAREST, ANTIALIAS
+resize_filter = string(default="ANTIALIAS")
+
[media_type:mediagoblin.media_types.video]
# Should we keep the original file?
keep_original = boolean(default=False)
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index 80ec5269..b25d577d 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -184,3 +184,14 @@ def fix_CollectionItem_v0_constraint(db_conn):
pass
db_conn.commit()
+
+
+@RegisterMigration(8, MIGRATIONS)
+def add_license_preference(db):
+ metadata = MetaData(bind=db.bind)
+
+ user_table = inspect_table(metadata, 'core__users')
+
+ col = Column('license_preference', Unicode)
+ col.create(user_table)
+ db.commit()
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index ea915ae5..de491e96 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -18,7 +18,7 @@
TODO: indexes on foreignkeys, where useful.
"""
-
+import logging
import datetime
import sys
@@ -34,6 +34,7 @@ from sqlalchemy.util import memoized_property
from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded
from mediagoblin.db.base import Base, DictReadAttrProxy, Session
from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin
+from mediagoblin.tools.files import delete_media_files
# It's actually kind of annoying how sqlalchemy-migrate does this, if
# I understand it right, but whatever. Anyway, don't remove this :P
@@ -42,6 +43,8 @@ from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin,
# this import-based meddling...
from migrate import changeset
+_log = logging.getLogger(__name__)
+
class User(Base, UserMixin):
"""
@@ -60,6 +63,7 @@ class User(Base, UserMixin):
# Intented to be nullable=False, but migrations would not work for it
# set to nullable=True implicitly.
wants_comment_notification = Column(Boolean, default=True)
+ license_preference = Column(Unicode)
verification_key = Column(Unicode)
is_admin = Column(Boolean, default=False, nullable=False)
url = Column(Unicode)
@@ -78,6 +82,25 @@ class User(Base, UserMixin):
'admin' if self.is_admin else 'user',
self.username)
+ def delete(self, **kwargs):
+ """Deletes a User and all related entries/comments/files/..."""
+ # Collections get deleted by relationships.
+
+ media_entries = MediaEntry.query.filter(MediaEntry.uploader == self.id)
+ for media in media_entries:
+ # TODO: Make sure that "MediaEntry.delete()" also deletes
+ # all related files/Comments
+ media.delete(del_orphan_tags=False, commit=False)
+
+ # Delete now unused tags
+ # TODO: import here due to cyclic imports!!! This cries for refactoring
+ from mediagoblin.db.util import clean_orphan_tags
+ clean_orphan_tags(commit=False)
+
+ # Delete user, pass through commit=False/True in kwargs
+ super(User, self).delete(**kwargs)
+ _log.info('Deleted user "{0}" account'.format(self.username))
+
class MediaEntry(Base, MediaEntryMixin):
"""
@@ -122,7 +145,6 @@ class MediaEntry(Base, MediaEntryMixin):
)
attachment_files_helper = relationship("MediaAttachmentFile",
- cascade="all, delete-orphan",
order_by="MediaAttachmentFile.created"
)
attachment_files = association_proxy("attachment_files_helper", "dict_view",
@@ -131,7 +153,7 @@ class MediaEntry(Base, MediaEntryMixin):
)
tags_helper = relationship("MediaTag",
- cascade="all, delete-orphan"
+ cascade="all, delete-orphan" # should be automatically deleted
)
tags = association_proxy("tags_helper", "dict_view",
creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
@@ -216,6 +238,37 @@ class MediaEntry(Base, MediaEntryMixin):
id=self.id,
title=safe_title)
+ def delete(self, del_orphan_tags=True, **kwargs):
+ """Delete MediaEntry and all related files/attachments/comments
+
+ This will *not* automatically delete unused collections, which
+ can remain empty...
+
+ :param del_orphan_tags: True/false if we delete unused Tags too
+ :param commit: True/False if this should end the db transaction"""
+ # User's CollectionItems are automatically deleted via "cascade".
+ # Delete all the associated comments
+ for comment in self.get_comments():
+ comment.delete(commit=False)
+
+ # Delete all related files/attachments
+ try:
+ delete_media_files(self)
+ except OSError, error:
+ # Returns list of files we failed to delete
+ _log.error('No such files from the user "{1}" to delete: '
+ '{0}'.format(str(error), self.get_uploader))
+ _log.info('Deleted Media entry id "{0}"'.format(self.id))
+ # Related MediaTag's are automatically cleaned, but we might
+ # want to clean out unused Tag's too.
+ if del_orphan_tags:
+ # TODO: Import here due to cyclic imports!!!
+ # This cries for refactoring
+ from mediagoblin.db.util import clean_orphan_tags
+ clean_orphan_tags(commit=False)
+ # pass through commit=False/True in kwargs
+ super(MediaEntry, self).delete(**kwargs)
+
class FileKeynames(Base):
"""
@@ -344,6 +397,10 @@ class MediaComment(Base, MediaCommentMixin):
class Collection(Base, CollectionMixin):
+ """An 'album' or 'set' of media by a user.
+
+ On deletion, contained CollectionItems get automatically reaped via
+ SQL cascade"""
__tablename__ = "core__collections"
id = Column(Integer, primary_key=True)
@@ -353,11 +410,16 @@ class Collection(Base, CollectionMixin):
index=True)
description = Column(UnicodeText)
creator = Column(Integer, ForeignKey(User.id), nullable=False)
+ # TODO: No of items in Collection. Badly named, can we migrate to num_items?
items = Column(Integer, default=0)
- get_creator = relationship(User)
+ # Cascade: Collections are owned by their creator. So do the full thing.
+ get_creator = relationship(User,
+ backref=backref("collections",
+ cascade="all, delete-orphan"))
def get_collection_items(self, ascending=False):
+ #TODO, is this still needed with self.collection_items being available?
order_col = CollectionItem.position
if not ascending:
order_col = desc(order_col)
@@ -375,7 +437,12 @@ class CollectionItem(Base, CollectionItemMixin):
note = Column(UnicodeText, nullable=True)
added = Column(DateTime, nullable=False, default=datetime.datetime.now)
position = Column(Integer)
- in_collection = relationship("Collection")
+
+ # Cascade: CollectionItems are owned by their Collection. So do the full thing.
+ in_collection = relationship(Collection,
+ backref=backref(
+ "collection_items",
+ cascade="all, delete-orphan"))
get_media_entry = relationship(MediaEntry)
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/decorators.py b/mediagoblin/decorators.py
index 5533e81d..09235614 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -20,6 +20,7 @@ from urlparse import urljoin
from werkzeug.exceptions import Forbidden, NotFound
from werkzeug.urls import url_quote
+from mediagoblin import mg_globals as mgg
from mediagoblin.db.models import MediaEntry, User
from mediagoblin.tools.response import redirect, render_404
@@ -31,11 +32,11 @@ def require_active_login(controller):
@wraps(controller)
def new_controller_func(request, *args, **kwargs):
if request.user and \
- request.user.get('status') == u'needs_email_verification':
+ request.user.status == u'needs_email_verification':
return redirect(
request, 'mediagoblin.user_pages.user_home',
user=request.user.username)
- elif not request.user or request.user.get('status') != u'active':
+ elif not request.user or request.user.status != u'active':
next_url = urljoin(
request.urlgen('mediagoblin.auth.login',
qualified=True),
@@ -69,7 +70,7 @@ def user_may_delete_media(controller):
"""
@wraps(controller)
def wrapper(request, *args, **kwargs):
- uploader_id = MediaEntry.query.get(request.matchdict['media']).uploader
+ uploader_id = kwargs['media'].uploader
if not (request.user.is_admin or
request.user.id == uploader_id):
raise Forbidden()
@@ -209,12 +210,27 @@ def get_media_entry_by_id(controller):
@wraps(controller)
def wrapper(request, *args, **kwargs):
media = MediaEntry.query.filter_by(
- id=request.matchdict['media'],
+ id=request.matchdict['media_id'],
state=u'processed').first()
# Still no media? Okay, 404.
if not media:
return render_404(request)
+ given_username = request.matchdict.get('user')
+ if given_username and (given_username != media.get_uploader.username):
+ return render_404(request)
+
return controller(request, media=media, *args, **kwargs)
return wrapper
+
+
+def get_workbench(func):
+ """Decorator, passing in a workbench as kwarg which is cleaned up afterwards"""
+
+ @wraps(func)
+ def new_func(*args, **kwargs):
+ with mgg.workbench_manager.create() as workbench:
+ return func(*args, workbench=workbench, **kwargs)
+
+ return new_func
diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py
index 293c3bb2..2673967b 100644
--- a/mediagoblin/edit/forms.py
+++ b/mediagoblin/edit/forms.py
@@ -65,8 +65,19 @@ class EditAccountForm(wtforms.Form):
"Enter your old password to prove you own this account."))
new_password = wtforms.PasswordField(
_('New password'),
- [wtforms.validators.Length(min=6, max=30)],
+ [
+ wtforms.validators.Optional(),
+ wtforms.validators.Length(min=6, max=30)
+ ],
id="password")
+ license_preference = wtforms.SelectField(
+ _('License preference'),
+ [
+ wtforms.validators.Optional(),
+ wtforms.validators.AnyOf([lic[0] for lic in licenses_as_choices()]),
+ ],
+ choices=licenses_as_choices(),
+ description=_('This will be your default license on upload forms.'))
wants_comment_notification = wtforms.BooleanField(
label=_("Email me when others comment on my media"))
diff --git a/mediagoblin/edit/routing.py b/mediagoblin/edit/routing.py
index d382e549..035a766f 100644
--- a/mediagoblin/edit/routing.py
+++ b/mediagoblin/edit/routing.py
@@ -22,3 +22,5 @@ add_route('mediagoblin.edit.legacy_edit_profile', '/edit/profile/',
'mediagoblin.edit.views:legacy_edit_profile')
add_route('mediagoblin.edit.account', '/edit/account/',
'mediagoblin.edit.views:edit_account')
+add_route('mediagoblin.edit.delete_account', '/edit/account/delete/',
+ 'mediagoblin.edit.views:delete_account')
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index 646a9e5b..25a617fd 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -27,6 +27,7 @@ from mediagoblin.auth import lib as auth_lib
from mediagoblin.edit import forms
from mediagoblin.edit.lib import may_edit_media
from mediagoblin.decorators import (require_active_login, active_user_from_url,
+ get_media_entry_by_id,
get_user_media_entry, user_may_alter_collection, get_user_collection)
from mediagoblin.tools.response import render_to_response, redirect
from mediagoblin.tools.translate import pass_to_ugettext as _
@@ -38,7 +39,7 @@ from mediagoblin.db.util import check_media_slug_used, check_collection_slug_use
import mimetypes
-@get_user_media_entry
+@get_media_entry_by_id
@require_active_login
def edit_media(request, media):
if not may_edit_media(request, media):
@@ -190,8 +191,8 @@ def edit_profile(request, url_user=None):
user = url_user
form = forms.EditProfileForm(request.form,
- url=user.get('url'),
- bio=user.get('bio'))
+ url=user.url,
+ bio=user.bio)
if request.method == 'POST' and form.validate():
user.url = unicode(request.form['url'])
@@ -217,45 +218,42 @@ def edit_profile(request, url_user=None):
def edit_account(request):
user = request.user
form = forms.EditAccountForm(request.form,
- wants_comment_notification=user.get('wants_comment_notification'))
+ wants_comment_notification=user.wants_comment_notification,
+ license_preference=user.license_preference)
if request.method == 'POST':
form_validated = form.validate()
- #if the user has not filled in the new or old password fields
- if not form.new_password.data and not form.old_password.data:
- if form.wants_comment_notification.validate(form):
- user.wants_comment_notification = \
- form.wants_comment_notification.data
- user.save()
- messages.add_message(request,
- messages.SUCCESS,
- _("Account settings saved"))
- return redirect(request,
- 'mediagoblin.user_pages.user_home',
- user=user.username)
-
- #so the user has filled in one or both of the password fields
- else:
- if form_validated:
- password_matches = auth_lib.bcrypt_check_password(
- form.old_password.data,
- user.pw_hash)
- if password_matches:
- #the entire form validates and the password matches
- user.pw_hash = auth_lib.bcrypt_gen_password_hash(
- form.new_password.data)
- user.wants_comment_notification = \
- form.wants_comment_notification.data
- user.save()
- messages.add_message(request,
- messages.SUCCESS,
- _("Account settings saved"))
- return redirect(request,
- 'mediagoblin.user_pages.user_home',
- user=user.username)
- else:
- form.old_password.errors.append(_('Wrong password'))
+ if form_validated and \
+ form.wants_comment_notification.validate(form):
+ user.wants_comment_notification = \
+ form.wants_comment_notification.data
+
+ if form_validated and \
+ form.new_password.data or form.old_password.data:
+ password_matches = auth_lib.bcrypt_check_password(
+ form.old_password.data,
+ user.pw_hash)
+ if password_matches:
+ #the entire form validates and the password matches
+ user.pw_hash = auth_lib.bcrypt_gen_password_hash(
+ form.new_password.data)
+ else:
+ form.old_password.errors.append(_('Wrong password'))
+
+ if form_validated and \
+ form.license_preference.validate(form):
+ user.license_preference = \
+ form.license_preference.data
+
+ if form_validated and not form.errors:
+ user.save()
+ messages.add_message(request,
+ messages.SUCCESS,
+ _("Account settings saved"))
+ return redirect(request,
+ 'mediagoblin.user_pages.user_home',
+ user=user.username)
return render_to_response(
request,
@@ -265,6 +263,37 @@ def edit_account(request):
@require_active_login
+def delete_account(request):
+ """Delete a user completely"""
+ user = request.user
+ if request.method == 'POST':
+ if request.form.get(u'confirmed'):
+ # Form submitted and confirmed. Actually delete the user account
+ # Log out user and delete cookies etc.
+ # TODO: Should we be using MG.auth.views.py:logout for this?
+ request.session.delete()
+
+ # Delete user account and all related media files etc....
+ request.user.delete()
+
+ # We should send a message that the user has been deleted
+ # successfully. But we just deleted the session, so we
+ # can't...
+ return redirect(request, 'index')
+
+ else: # Did not check the confirmation box...
+ messages.add_message(
+ request, messages.WARNING,
+ _('You need to confirm the deletion of your account.'))
+
+ # No POST submission or not confirmed, just show page
+ return render_to_response(
+ request,
+ 'mediagoblin/edit/delete_account.html',
+ {'user': user})
+
+
+@require_active_login
@user_may_alter_collection
@get_user_collection
def edit_collection(request, collection):
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/ascii/processing.py b/mediagoblin/media_types/ascii/processing.py
index 04d1166c..254717eb 100644
--- a/mediagoblin/media_types/ascii/processing.py
+++ b/mediagoblin/media_types/ascii/processing.py
@@ -19,6 +19,7 @@ import Image
import logging
from mediagoblin import mg_globals as mgg
+from mediagoblin.decorators import get_workbench
from mediagoblin.processing import create_pub_filepath
from mediagoblin.media_types.ascii import asciitoimage
@@ -38,12 +39,14 @@ def sniff_handler(media_file, **kw):
return False
-def process_ascii(entry):
- '''
- Code to process a txt file
- '''
+@get_workbench
+def process_ascii(entry, workbench=None):
+ """Code to process a txt file. Will be run by celery.
+
+ A Workbench() represents a local tempory dir. It is automatically
+ cleaned up when this function exits.
+ """
ascii_config = mgg.global_config['media_type:mediagoblin.media_types.ascii']
- workbench = mgg.workbench_manager.create_workbench()
# Conversions subdirectory to avoid collisions
conversions_subdir = os.path.join(
workbench.dir, 'conversions')
diff --git a/mediagoblin/media_types/audio/processing.py b/mediagoblin/media_types/audio/processing.py
index aee843d5..e12cefe6 100644
--- a/mediagoblin/media_types/audio/processing.py
+++ b/mediagoblin/media_types/audio/processing.py
@@ -15,10 +15,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
-import tempfile
+from tempfile import NamedTemporaryFile
import os
from mediagoblin import mg_globals as mgg
+from mediagoblin.decorators import get_workbench
from mediagoblin.processing import (create_pub_filepath, BadMediaFail,
FilenameBuilder, ProgressCallback)
@@ -42,10 +43,14 @@ def sniff_handler(media_file, **kw):
return False
-def process_audio(entry):
- audio_config = mgg.global_config['media_type:mediagoblin.media_types.audio']
+@get_workbench
+def process_audio(entry, workbench=None):
+ """Code to process uploaded audio. Will be run by celery.
- workbench = mgg.workbench_manager.create_workbench()
+ A Workbench() represents a local tempory dir. It is automatically
+ cleaned up when this function exits.
+ """
+ audio_config = mgg.global_config['media_type:mediagoblin.media_types.audio']
queued_filepath = entry.queued_media_file
queued_filename = workbench.localized_file(
@@ -73,7 +78,7 @@ def process_audio(entry):
transcoder = AudioTranscoder()
- with tempfile.NamedTemporaryFile() as webm_audio_tmp:
+ with NamedTemporaryFile(dir=workbench.dir) as webm_audio_tmp:
progress_callback = ProgressCallback(entry)
transcoder.transcode(
@@ -99,7 +104,7 @@ def process_audio(entry):
original=os.path.splitext(
queued_filepath[-1])[0]))
- with tempfile.NamedTemporaryFile(suffix='.ogg') as wav_tmp:
+ with NamedTemporaryFile(dir=workbench.dir, suffix='.ogg') as wav_tmp:
_log.info('Creating OGG source for spectrogram')
transcoder.transcode(
queued_filename,
@@ -109,7 +114,7 @@ def process_audio(entry):
thumbnailer = AudioThumbnailer()
- with tempfile.NamedTemporaryFile(suffix='.jpg') as spectrogram_tmp:
+ with NamedTemporaryFile(dir=workbench.dir, suffix='.jpg') as spectrogram_tmp:
thumbnailer.spectrogram(
wav_tmp.name,
spectrogram_tmp.name,
@@ -122,7 +127,7 @@ def process_audio(entry):
entry.media_files['spectrogram'] = spectrogram_filepath
- with tempfile.NamedTemporaryFile(suffix='.jpg') as thumb_tmp:
+ with NamedTemporaryFile(dir=workbench.dir, suffix='.jpg') as thumb_tmp:
thumbnailer.thumbnail_spectrogram(
spectrogram_tmp.name,
thumb_tmp.name,
@@ -143,6 +148,3 @@ def process_audio(entry):
entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg']
mgg.queue_store.delete_file(queued_filepath)
-
- # clean up workbench
- workbench.destroy_self()
diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py
index bf464069..99be848f 100644
--- a/mediagoblin/media_types/image/processing.py
+++ b/mediagoblin/media_types/image/processing.py
@@ -19,6 +19,7 @@ import os
import logging
from mediagoblin import mg_globals as mgg
+from mediagoblin.decorators import get_workbench
from mediagoblin.processing import BadMediaFail, \
create_pub_filepath, FilenameBuilder
from mediagoblin.tools.exif import exif_fix_image_orientation, \
@@ -27,6 +28,12 @@ from mediagoblin.tools.exif import exif_fix_image_orientation, \
_log = logging.getLogger(__name__)
+PIL_FILTERS = {
+ 'NEAREST': Image.NEAREST,
+ 'BILINEAR': Image.BILINEAR,
+ 'BICUBIC': Image.BICUBIC,
+ 'ANTIALIAS': Image.ANTIALIAS}
+
def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
size_limits=(0, 0)):
@@ -46,7 +53,19 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
except IOError:
raise BadMediaFail()
resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation
- resized.thumbnail(new_size, Image.ANTIALIAS)
+
+ filter_config = \
+ mgg.global_config['media_type:mediagoblin.media_types.image']\
+ ['resize_filter']
+
+ try:
+ resize_filter = PIL_FILTERS[filter_config.upper()]
+ except KeyError:
+ raise Exception('Filter "{0}" not found, choose one of {1}'.format(
+ unicode(filter_config),
+ u', '.join(PIL_FILTERS.keys())))
+
+ resized.thumbnail(new_size, resize_filter)
# Copy the new file to the conversion subdir, then remotely.
tmp_resized_filename = os.path.join(workdir, new_path[-1])
@@ -76,11 +95,13 @@ def sniff_handler(media_file, **kw):
return False
-def process_image(entry):
- """
- Code to process an image
+@get_workbench
+def process_image(entry, workbench=None):
+ """Code to process an image. Will be run by celery.
+
+ A Workbench() represents a local tempory dir. It is automatically
+ cleaned up when this function exits.
"""
- workbench = mgg.workbench_manager.create_workbench()
# Conversions subdirectory to avoid collisions
conversions_subdir = os.path.join(
workbench.dir, 'conversions')
@@ -147,8 +168,6 @@ def process_image(entry):
gps_data['gps_' + key] = gps_data.pop(key)
entry.media_data_init(**gps_data)
- # clean up workbench
- workbench.destroy_self()
if __name__ == '__main__':
import sys
diff --git a/mediagoblin/media_types/stl/processing.py b/mediagoblin/media_types/stl/processing.py
index cd949e2a..3089f295 100644
--- a/mediagoblin/media_types/stl/processing.py
+++ b/mediagoblin/media_types/stl/processing.py
@@ -21,6 +21,7 @@ import subprocess
import pkg_resources
from mediagoblin import mg_globals as mgg
+from mediagoblin.decorators import get_workbench
from mediagoblin.processing import create_pub_filepath, \
FilenameBuilder
@@ -75,11 +76,13 @@ def blender_render(config):
env=env)
-def process_stl(entry):
- """
- Code to process an stl or obj model.
+@get_workbench
+def process_stl(entry, workbench=None):
+ """Code to process an stl or obj model. Will be run by celery.
+
+ A Workbench() represents a local tempory dir. It is automatically
+ cleaned up when this function exits.
"""
- workbench = mgg.workbench_manager.create_workbench()
queued_filepath = entry.queued_media_file
queued_filename = workbench.localized_file(
mgg.queue_store, queued_filepath, 'source')
@@ -164,7 +167,7 @@ def process_stl(entry):
# Remove queued media file from storage and database
mgg.queue_store.delete_file(queued_filepath)
entry.queued_media_file = []
-
+
# Insert media file information into database
media_files_dict = entry.setdefault('media_files', {})
media_files_dict[u'original'] = model_filepath
@@ -185,6 +188,3 @@ def process_stl(entry):
"file_type" : ext,
}
entry.media_data_init(**dimensions)
-
- # clean up workbench
- workbench.destroy_self()
diff --git a/mediagoblin/media_types/video/processing.py b/mediagoblin/media_types/video/processing.py
index aa6a25df..4c9f0131 100644
--- a/mediagoblin/media_types/video/processing.py
+++ b/mediagoblin/media_types/video/processing.py
@@ -14,10 +14,11 @@
# 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 tempfile
+from tempfile import NamedTemporaryFile
import logging
from mediagoblin import mg_globals as mgg
+from mediagoblin.decorators import get_workbench
from mediagoblin.processing import \
create_pub_filepath, FilenameBuilder, BaseProcessingFail, ProgressCallback
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
@@ -51,16 +52,17 @@ def sniff_handler(media_file, **kw):
return False
-
-def process_video(entry):
+@get_workbench
+def process_video(entry, workbench=None):
"""
Process a video entry, transcode the queued media files (originals) and
create a thumbnail for the entry.
+
+ A Workbench() represents a local tempory dir. It is automatically
+ cleaned up when this function exits.
"""
video_config = mgg.global_config['media_type:mediagoblin.media_types.video']
- workbench = mgg.workbench_manager.create_workbench()
-
queued_filepath = entry.queued_media_file
queued_filename = workbench.localized_file(
mgg.queue_store, queued_filepath,
@@ -73,9 +75,8 @@ def process_video(entry):
thumbnail_filepath = create_pub_filepath(
entry, name_builder.fill('{basename}.thumbnail.jpg'))
- # Create a temporary file for the video destination
- tmp_dst = tempfile.NamedTemporaryFile()
-
+ # 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)
@@ -86,21 +87,20 @@ def process_video(entry):
vorbis_quality=video_config['vorbis_quality'],
progress_callback=progress_callback)
- # Push transcoded video to public storage
- _log.debug('Saving medium...')
- 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 = tempfile.NamedTemporaryFile(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
@@ -109,29 +109,16 @@ def process_video(entry):
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...')
- 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)
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/static/css/base.css b/mediagoblin/static/css/base.css
index 2085cfc9..04b4ee28 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -113,10 +113,12 @@ input, textarea {
header {
width: 100%;
+ max-width: 940px;
+ margin-left: auto;
+ margin-right: auto;
padding: 0;
margin-bottom: 42px;
- background-color: #303030;
- border-bottom: 1px solid #252525;
+ border-bottom: 1px solid #333;
}
.header_right {
@@ -125,19 +127,18 @@ header {
float: right;
}
-.header_right ul {
- display: none;
- position: absolute;
- top: 42px;
- right: 0px;
- background: #252525;
- padding: 20px;
+.header_dropdown {
+ margin-bottom: 20px;
}
-.header_right li {
+.header_dropdown li {
list-style: none;
}
+.dropdown_title {
+ font-size: 20px;
+}
+
a.logo {
color: #fff;
font-weight: bold;
@@ -145,7 +146,7 @@ a.logo {
.logo img {
vertical-align: middle;
- margin: 6px 8px;
+ margin: 6px 8px 6px 0;
}
.mediagoblin_content {
@@ -331,6 +332,12 @@ textarea#description, textarea#bio {
height: 100px;
}
+.delete {
+ margin-top: 36px;
+ display: block;
+ text-align: center;
+}
+
/* comments */
.comment_wrapper {
diff --git a/mediagoblin/static/js/header_dropdown.js b/mediagoblin/static/js/header_dropdown.js
new file mode 100644
index 00000000..1b2fb00f
--- /dev/null
+++ b/mediagoblin/static/js/header_dropdown.js
@@ -0,0 +1,27 @@
+/**
+ * GNU MediaGoblin -- federated, autonomous media hosting
+ * Copyright (C) 2011, 2012 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/>.
+ */
+
+$(document).ready(function(){
+ $(".header_dropdown").hide();
+ $(".header_dropdown_up").hide();
+ $(".header_dropdown_down,.header_dropdown_up").click(function() {
+ $(".header_dropdown_down").toggle();
+ $(".header_dropdown_up").toggle();
+ $(".header_dropdown").slideToggle();
+ });
+});
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 2d609b31..def7e839 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
@@ -41,7 +40,8 @@ def submit_start(request):
"""
First view for submitting a file.
"""
- submit_form = submit_forms.SubmitStartForm(request.form)
+ submit_form = submit_forms.SubmitStartForm(request.form,
+ license=request.user.license_preference)
if request.method == 'POST' and submit_form.validate():
if not ('file' in request.files
@@ -90,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/base.html b/mediagoblin/templates/mediagoblin/base.html
index 0a9a56d3..98b32f4a 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -28,6 +28,9 @@
<link rel="shortcut icon"
href="{{ request.staticdirect('/images/goblin.ico') }}" />
<script src="{{ request.staticdirect('/js/extlib/jquery.js') }}"></script>
+ <script type="text/javascript"
+ src="{{ request.staticdirect('/js/header_dropdown.js') }}"></script>
+
<!--[if lt IE 9]>
<script src="{{ request.staticdirect('/js/extlib/html5shiv.js') }}"></script>
<![endif]-->
@@ -45,21 +48,16 @@
{% block mediagoblin_header_title %}{% endblock %}
<div class="header_right">
{% if request.user %}
- {% trans
- user_url=request.urlgen('mediagoblin.user_pages.user_home',
- user= request.user.username),
- user_name=request.user.username -%}
- <a href="{{ user_url }}">{{ user_name }}</a>'s account
- {%- endtrans %}
- (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>)
{% if request.user and request.user.status == 'active' %}
- <a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">{% trans %}Add media{% endtrans %}</a>
+ <div class="button_action header_dropdown_down">&#9660;</div>
+ <div class="button_action header_dropdown_up">&#9650;</div>
{% elif request.user and request.user.status == "needs_email_verification" %}
{# the following link should only appear when verification is needed #}
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
user=request.user.username) }}"
class="button_action_highlight">
{% trans %}Verify your email!{% endtrans %}</a>
+ or <a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>
{% endif %}
{% else %}
<a href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{
@@ -68,6 +66,44 @@
{% endif %}
</div>
<div class="clear"></div>
+ {% if request.user and request.user.status == 'active' %}
+ <div class="header_dropdown">
+ <p>
+ <span class="dropdown_title">
+ {% trans user_url=request.urlgen('mediagoblin.user_pages.user_home',
+ user=request.user.username),
+ user_name=request.user.username -%}
+ <a href="{{ user_url }}">{{ user_name }}</a>'s account
+ {%- endtrans %}
+ </span>
+ (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>)
+ </p>
+ <ul>
+ <li><a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">
+ {%- trans %}Add media{% endtrans -%}
+ </a></li>
+ <li><a class="button_action" href="{{ request.urlgen('mediagoblin.submit.collection') }}">
+ {%- trans %}Create new collection{% endtrans -%}
+ </a></li>
+ <li><a href="{{ request.urlgen('mediagoblin.edit.account') }}">
+ {%- trans %}Change account settings{% endtrans -%}
+ </a></li>
+ <li><a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel',
+ user=request.user.username) }}">
+ {%- trans %}Media processing panel{% endtrans -%}
+ </a></li>
+ {% if request.user.is_admin %}
+ <li>Admin:
+ <ul>
+ <li><a href="{{ request.urlgen('mediagoblin.admin.panel') }}">
+ {%- trans %}Media processing panel{% endtrans -%}
+ </a></li>
+ </ul>
+ </li>
+ {% endif %}
+ </ul>
+ </div>
+ {% endif %}
</header>
{% endblock %}
<div class="container">
diff --git a/mediagoblin/templates/mediagoblin/edit/delete_account.html b/mediagoblin/templates/mediagoblin/edit/delete_account.html
new file mode 100644
index 00000000..84d0b580
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/edit/delete_account.html
@@ -0,0 +1,48 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+
+{% block mediagoblin_content %}
+
+ <form action="{{ request.urlgen('mediagoblin.edit.delete_account') }}"
+ method="POST" enctype="multipart/form-data">
+ <div class="form_box">
+ <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">
+ {%- trans %}Yes, really delete my account{% endtrans -%}
+ </label>
+ </p>
+
+ <div class="form_submit_buttons">
+ <a class="button_action" href="{{ request.urlgen(
+ 'mediagoblin.user_pages.user_home',
+ user=user.username) }}">{% trans %}Cancel{% endtrans %}</a>
+ {{ csrf_token }}
+ <input type="submit" value="{% trans %}Delete permanently{% endtrans %}" class="button_form" />
+ </div>
+ </div>
+ </form>
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/edit/edit.html b/mediagoblin/templates/mediagoblin/edit/edit.html
index 1f5b91f7..9a040095 100644
--- a/mediagoblin/templates/mediagoblin/edit/edit.html
+++ b/mediagoblin/templates/mediagoblin/edit/edit.html
@@ -29,7 +29,7 @@
<form action="{{ request.urlgen('mediagoblin.edit.edit_media',
user= media.get_uploader.username,
- media= media.id) }}"
+ media_id=media.id) }}"
method="POST" enctype="multipart/form-data">
<div class="form_box_xl edit_box">
<h1>{% trans media_title=media.title %}Editing {{ media_title }}{% endtrans %}</h1>
diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html
index 38d99893..4b980301 100644
--- a/mediagoblin/templates/mediagoblin/edit/edit_account.html
+++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html
@@ -47,10 +47,19 @@
<p>{{ form.wants_comment_notification }}
{{ form.wants_comment_notification.label }}</p>
</div>
+ <div class="form_field_input">
+ <p>{{ form.license_preference }}
+ {{ form.license_preference.label }}</p>
+ </div>
<div class="form_submit_buttons">
<input type="submit" value="{% trans %}Save changes{% endtrans %}" class="button_form" />
{{ csrf_token }}
</div>
</div>
</form>
+ <div class="delete">
+ <a href="{{ request.urlgen('mediagoblin.edit.delete_account') }}">
+ {%- trans %}Delete my account{% endtrans -%}
+ </a>
+ </div>
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/root.html b/mediagoblin/templates/mediagoblin/root.html
index 047dd2bb..5c6eb52f 100644
--- a/mediagoblin/templates/mediagoblin/root.html
+++ b/mediagoblin/templates/mediagoblin/root.html
@@ -21,33 +21,6 @@
{% block mediagoblin_content %}
{% if request.user %}
- {% if request.user.status == 'active' %}
- <h1>{% trans %}Actions{% endtrans %}</h1>
- <ul>
- <li><a href="{{ request.urlgen('mediagoblin.submit.start') }}">
- {%- trans %}Add media{% endtrans -%}
- </a></li>
- <li><a href="{{ request.urlgen('mediagoblin.submit.collection') }}">
- {%- trans %}Create new collection{% endtrans -%}
- </a></li>
- <li><a href="{{ request.urlgen('mediagoblin.edit.account') }}">
- {%- trans %}Change account settings{% endtrans -%}
- </a></li>
- <li><a href="{{ request.urlgen('mediagoblin.user_pages.processing_panel',
- user=request.user.username) }}">
- {%- trans %}Media processing panel{% endtrans -%}
- </a></li>
- {% if request.user.is_admin %}
- <li>Admin:
- <ul>
- <li><a href="{{ request.urlgen('mediagoblin.admin.panel') }}">
- {%- trans %}Media processing panel{% endtrans -%}
- </a></li>
- </ul>
- </li>
- {% endif %}
- </ul>
- {% endif %}
<h1>{% trans %}Explore{% endtrans %}</h1>
{% else %}
<h1>{% trans %}Hi there, welcome to this MediaGoblin site!{% endtrans %}</h1>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/collection_list.html b/mediagoblin/templates/mediagoblin/user_pages/collection_list.html
new file mode 100644
index 00000000..abf22623
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/user_pages/collection_list.html
@@ -0,0 +1,56 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 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/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% block title %}
+ {%- trans username=user.username -%}
+ {{ username }}'s collections
+ {%- endtrans %} &mdash; {{ super() }}
+{% endblock %}
+
+{% block mediagoblin_content -%}
+ <h1>
+ {%- trans username=user.username,
+ user_url=request.urlgen(
+ 'mediagoblin.user_pages.user_home',
+ user=user.username) -%}
+ <a href="{{ user_url }}">{{ username }}</a>'s collections
+ {%- endtrans %}
+ </h1>
+
+ {% if request.user %}
+ {% if request.user.status == 'active' %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.submit.collection',
+ user=user.username) }}">
+ {%- trans %}Create new collection{% endtrans -%}
+ </p>
+ {% endif %}
+ {% endif %}
+
+ <ul>
+ {% for coll in collections %}
+ {% set coll_url = request.urlgen(
+ 'mediagoblin.user_pages.user_collection',
+ user=user.username,
+ collection=coll.slug) %}
+ <li><a href="{{ coll_url }}">{{ coll.title }}</li>
+ {% endfor %}
+ </ul>
+
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index 11f2a2a1..7e184257 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -83,11 +83,11 @@
request.user.is_admin) %}
{% set edit_url = request.urlgen('mediagoblin.edit.edit_media',
user= media.get_uploader.username,
- media= media.id) %}
+ media_id=media.id) %}
<a class="button_action" href="{{ edit_url }}">{% trans %}Edit{% endtrans %}</a>
{% set delete_url = request.urlgen('mediagoblin.user_pages.media_confirm_delete',
user= media.get_uploader.username,
- media= media.id) %}
+ media_id=media.id) %}
<a class="button_action" href="{{ delete_url }}">{% trans %}Delete{% endtrans %}</a>
{% endif %}
{% autoescape False %}
@@ -104,10 +104,7 @@
{% if request.user %}
<form action="{{ request.urlgen('mediagoblin.user_pages.media_post_comment',
user= media.get_uploader.username,
- media=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>
+ media_id=media.id) }}" method="POST" id="form_comment">
{{ 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/templates/mediagoblin/user_pages/media_confirm_delete.html b/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
index 833f500d..d2a5655e 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media_confirm_delete.html
@@ -23,7 +23,7 @@
<form action="{{ request.urlgen('mediagoblin.user_pages.media_confirm_delete',
user=media.get_uploader.username,
- media=media.id) }}"
+ media_id=media.id) }}"
method="POST" enctype="multipart/form-data">
<div class="form_box">
<h1>
diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html
index 76bce1e2..71acd66c 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/user.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/user.html
@@ -118,6 +118,12 @@
</a>
{% endif %}
{% endif %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.user_pages.collection_list',
+ user=user.username) }}">
+ {%- trans %}Browse collections{% endtrans -%}
+ </a>
+ </p>
</div>
{% if media_entries.count() %}
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 cbdad649..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)
@@ -37,6 +37,24 @@ class TestUserEdit(object):
'password': self.user_password})
+ def test_user_deletion(self):
+ """Delete user via web interface"""
+ # Make sure user exists
+ assert User.query.filter_by(username=u'chris').first()
+
+ res = self.app.post('/edit/account/delete/', {'confirmed': 'y'})
+
+ # Make sure user has been deleted
+ assert User.query.filter_by(username=u'chris').first() == None
+
+ #TODO: make sure all corresponding items comments etc have been
+ # deleted too. Perhaps in submission test?
+
+ #Restore user at end of test
+ self.user = fixture_add_user(password = self.user_password)
+ self.login()
+
+
def test_change_password(self):
"""Test changing password correctly and incorrectly"""
# test that the password can be changed
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 faf4e744..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,23 @@ class TestSubmission:
media = self.check_media(request, {'title': u'Balanced Goblin'}, 1)
media_id = media.id
+ # 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)
comment_url = request.urlgen(
'mediagoblin.user_pages.media_post_comment',
- user=self.test_user.username, media=media_id)
+ user=self.test_user.username, media_id=media_id)
response = self.do_post({'comment_content': 'i love this test'},
url=comment_url, do_follow=True)[0]
self.check_comments(request, media_id, 1)
@@ -174,7 +186,7 @@ class TestSubmission:
# ---------------------------------------------------
delete_url = request.urlgen(
'mediagoblin.user_pages.media_confirm_delete',
- user=self.test_user.username, media=media_id)
+ user=self.test_user.username, media_id=media_id)
# Empty data means don't confirm
response = self.do_post({}, do_follow=True, url=delete_url)[0]
media = self.check_media(request, {'title': u'Balanced Goblin'}, 1)
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 04a74653..636c8689 100644
--- a/mediagoblin/tests/test_workbench.py
+++ b/mediagoblin/tests/test_workbench.py
@@ -18,7 +18,9 @@ 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
@@ -28,19 +30,20 @@ class TestWorkbench(object):
os.path.join(tempfile.gettempdir(), u'mgoblin_workbench_testing'))
def test_create_workbench(self):
- workbench = self.workbench_manager.create_workbench()
+ workbench = self.workbench_manager.create()
assert os.path.isdir(workbench.dir)
assert workbench.dir.startswith(self.workbench_manager.base_workbench_dir)
+ workbench.destroy()
def test_joinpath(self):
- this_workbench = self.workbench_manager.create_workbench()
+ this_workbench = self.workbench_manager.create()
tmpname = this_workbench.joinpath('temp.txt')
assert tmpname == os.path.join(this_workbench.dir, 'temp.txt')
- this_workbench.destroy_self()
+ this_workbench.destroy()
def test_destroy_workbench(self):
# kill a workbench
- this_workbench = self.workbench_manager.create_workbench()
+ this_workbench = self.workbench_manager.create()
tmpfile_name = this_workbench.joinpath('temp.txt')
tmpfile = file(tmpfile_name, 'w')
with tmpfile:
@@ -49,14 +52,14 @@ class TestWorkbench(object):
assert os.path.exists(tmpfile_name)
wb_dir = this_workbench.dir
- this_workbench.destroy_self()
+ this_workbench.destroy()
assert not os.path.exists(tmpfile_name)
assert not os.path.exists(wb_dir)
def test_localized_file(self):
tmpdir, this_storage = get_tmp_filestorage()
- this_workbench = self.workbench_manager.create_workbench()
-
+ this_workbench = self.workbench_manager.create()
+
# Write a brand new file
filepath = ['dir1', 'dir2', 'ourfile.txt']
@@ -78,7 +81,7 @@ class TestWorkbench(object):
filename = this_workbench.localized_file(this_storage, filepath)
assert filename == os.path.join(
this_workbench.dir, 'ourfile.txt')
-
+
# fake remote file storage, filename_if_copying set
filename = this_workbench.localized_file(
this_storage, filepath, 'thisfile')
@@ -91,3 +94,18 @@ class TestWorkbench(object):
this_storage, filepath, 'thisfile.text', False)
assert filename == os.path.join(
this_workbench.dir, 'thisfile.text')
+
+ def test_workbench_decorator(self):
+ """Test @get_workbench decorator and automatic cleanup"""
+ # The decorator needs mg_globals.workbench_manager
+ setup_globals(workbench_manager=self.workbench_manager)
+
+ @get_workbench
+ def create_it(workbench=None):
+ # workbench dir exists?
+ assert os.path.isdir(workbench.dir)
+ return workbench.dir
+
+ benchdir = create_it()
+ # workbench dir has been cleaned up automatically?
+ assert not os.path.isdir(benchdir)
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 2331b551..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
# ----------------------
@@ -119,7 +115,7 @@ class Workbench(object):
return full_dest_filename
- def destroy_self(self):
+ def destroy(self):
"""
Destroy this workbench! Deletes the directory and all its contents!
@@ -127,18 +123,33 @@ class Workbench(object):
"""
# just in case
workbench = os.path.abspath(self.dir)
-
shutil.rmtree(workbench)
-
del self.dir
+ def __enter__(self):
+ """Make Workbench a context manager so we can use `with Workbench() as bench:`"""
+ return self
+
+ def __exit__(self, *args):
+ """Clean up context manager, aka ourselves, deleting the workbench"""
+ self.destroy()
+
class WorkbenchManager(object):
"""
A system for generating and destroying workbenches.
- Workbenches are actually just subdirectories of a temporary storage space
- for during the processing stage.
+ Workbenches are actually just subdirectories of a (local) temporary
+ storage space for during the processing stage. The preferred way to
+ create them is to use:
+
+ with workbenchmger.create() as workbench:
+ do stuff...
+
+ This will automatically clean up all temporary directories even in
+ case of an exceptions. Also check the
+ @mediagoblin.decorators.get_workbench decorator for a convenient
+ wrapper.
"""
def __init__(self, base_workbench_dir):
@@ -146,7 +157,7 @@ class WorkbenchManager(object):
if not os.path.exists(self.base_workbench_dir):
os.makedirs(self.base_workbench_dir)
- def create_workbench(self):
+ def create(self):
"""
Create and return the path to a new workbench (directory).
"""
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 63bf5c2a..2b228355 100644
--- a/mediagoblin/user_pages/routing.py
+++ b/mediagoblin/user_pages/routing.py
@@ -24,12 +24,12 @@ add_route('mediagoblin.user_pages.media_home',
'mediagoblin.user_pages.views:media_home')
add_route('mediagoblin.user_pages.media_confirm_delete',
- '/u/<string:user>/m/<string:media>/confirm-delete/',
+ '/u/<string:user>/m/<int:media_id>/confirm-delete/',
'mediagoblin.user_pages.views:media_confirm_delete')
# Submission handling of new comments. TODO: only allow for POST methods
add_route('mediagoblin.user_pages.media_post_comment',
- '/u/<string:user>/m/<string:media>/comment/add/',
+ '/u/<string:user>/m/<int:media_id>/comment/add/',
'mediagoblin.user_pages.views:media_post_comment')
add_route('mediagoblin.user_pages.user_gallery',
@@ -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',
@@ -48,6 +48,10 @@ add_route('mediagoblin.user_pages.media_collect',
'/u/<string:user>/m/<string:media>/collect/',
'mediagoblin.user_pages.views:media_collect')
+add_route('mediagoblin.user_pages.collection_list',
+ '/u/<string:user>/collections/',
+ 'mediagoblin.user_pages.views:collection_list')
+
add_route('mediagoblin.user_pages.user_collection',
'/u/<string:user>/collection/<string:collection>/',
'mediagoblin.user_pages.views:user_collection')
@@ -74,7 +78,7 @@ add_route('mediagoblin.user_pages.processing_panel',
# Stray edit routes
add_route('mediagoblin.edit.edit_media',
- '/u/<string:user>/m/<string:media>/edit/',
+ '/u/<string:user>/m/<int:media_id>/edit/',
'mediagoblin.edit.views:edit_media')
add_route('mediagoblin.edit.attachments',
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index f115c3b8..dea47fbf 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -23,11 +23,11 @@ from mediagoblin.db.models import (MediaEntry, Collection, CollectionItem,
from mediagoblin.tools.response import render_to_response, render_404, redirect
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.pagination import Pagination
-from mediagoblin.tools.files import delete_media_files
from mediagoblin.user_pages import forms as user_forms
from mediagoblin.user_pages.lib import send_comment_email
from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
+ get_media_entry_by_id,
require_active_login, user_may_delete_media, user_may_alter_collection,
get_user_collection, get_user_collection_item, active_user_from_url)
@@ -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(
@@ -138,7 +139,7 @@ def media_home(request, media, page, **kwargs):
'app_config': mg_globals.app_config})
-@get_user_media_entry
+@get_media_entry_by_id
@require_active_login
def media_post_comment(request, media):
"""
@@ -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(
@@ -258,7 +263,7 @@ def media_collect(request, media):
#TODO: Why does @user_may_delete_media not implicate @require_active_login?
-@get_user_media_entry
+@get_media_entry_by_id
@require_active_login
@user_may_delete_media
def media_confirm_delete(request, media):
@@ -268,21 +273,7 @@ def media_confirm_delete(request, media):
if request.method == 'POST' and form.validate():
if form.confirm.data is True:
username = media.get_uploader.username
-
- # Delete all the associated comments
- for comment in media.get_comments():
- comment.delete()
-
- # Delete all files on the public storage
- try:
- delete_media_files(media)
- except OSError, error:
- _log.error('No such files from the user "{1}"'
- ' to delete: {0}'.format(str(error), username))
- messages.add_message(request, messages.ERROR,
- _('Some of the files with this entry seem'
- ' to be missing. Deleting anyway.'))
-
+ # Delete MediaEntry and all related files, comments etc.
media.delete()
messages.add_message(
request, messages.SUCCESS, _('You deleted the media.'))
@@ -337,6 +328,19 @@ def user_collection(request, page, url_user=None):
'pagination': pagination})
+@active_user_from_url
+def collection_list(request, url_user=None):
+ """A User-defined Collection"""
+ collections = Collection.query.filter_by(
+ get_creator=url_user)
+
+ return render_to_response(
+ request,
+ 'mediagoblin/user_pages/collection_list.html',
+ {'user': url_user,
+ 'collections': collections})
+
+
@get_user_collection_item
@require_active_login
@user_may_alter_collection