aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README4
-rwxr-xr-xdestroy_environment.py22
-rw-r--r--docs/deploymenthowto.rst5
-rw-r--r--docs/git.rst4
-rw-r--r--docs/hackinghowto.rst25
-rwxr-xr-xlazyserver.sh14
-rwxr-xr-xmaketarball.sh39
-rw-r--r--mediagoblin.ini3
-rw-r--r--mediagoblin/app.py65
-rw-r--r--mediagoblin/auth/lib.py2
-rw-r--r--mediagoblin/auth/routing.py6
-rw-r--r--mediagoblin/auth/views.py59
-rw-r--r--mediagoblin/config_spec.ini7
-rw-r--r--mediagoblin/db/indexes.py10
-rw-r--r--mediagoblin/db/migrations.py79
-rw-r--r--mediagoblin/db/models.py153
-rw-r--r--mediagoblin/db/open.py36
-rw-r--r--mediagoblin/db/util.py196
-rw-r--r--mediagoblin/decorators.py5
-rw-r--r--mediagoblin/edit/__init__.py17
-rw-r--r--mediagoblin/edit/forms.py3
-rw-r--r--mediagoblin/edit/views.py16
-rw-r--r--mediagoblin/gmg_commands/__init__.py4
-rw-r--r--mediagoblin/gmg_commands/migrate.py44
-rw-r--r--mediagoblin/gmg_commands/users.py16
-rw-r--r--mediagoblin/gmg_commands/wipealldata.py51
-rw-r--r--mediagoblin/init/__init__.py99
-rw-r--r--mediagoblin/init/celery/__init__.py2
-rw-r--r--mediagoblin/mg_globals.py21
-rw-r--r--mediagoblin/process_media/__init__.py33
-rw-r--r--mediagoblin/static/css/base.css242
-rw-r--r--mediagoblin/static/images/background.pngbin0 -> 6336 bytes
-rw-r--r--mediagoblin/static/images/icon.pngbin1670 -> 0 bytes
-rw-r--r--mediagoblin/static/images/logo.pngbin0 -> 839 bytes
-rw-r--r--mediagoblin/static/images/navigation_end.pngbin0 -> 718 bytes
-rw-r--r--mediagoblin/static/images/navigation_left.pngbin0 -> 406 bytes
-rw-r--r--mediagoblin/static/images/navigation_right.pngbin0 -> 383 bytes
-rw-r--r--mediagoblin/static/images/pagination_left.pngbin0 -> 252 bytes
-rw-r--r--mediagoblin/static/images/pagination_right.pngbin0 -> 249 bytes
-rw-r--r--mediagoblin/submit/__init__.py17
-rw-r--r--mediagoblin/submit/routing.py5
-rw-r--r--mediagoblin/submit/views.py5
-rw-r--r--mediagoblin/templates/mediagoblin/auth/login.html4
-rw-r--r--mediagoblin/templates/mediagoblin/auth/register_success.html25
-rw-r--r--mediagoblin/templates/mediagoblin/auth/verification_needed.html29
-rw-r--r--mediagoblin/templates/mediagoblin/base.html60
-rw-r--r--mediagoblin/templates/mediagoblin/root.html10
-rw-r--r--mediagoblin/templates/mediagoblin/submit/success.html22
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html28
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/user.html75
-rw-r--r--mediagoblin/templates/mediagoblin/utils/pagination.html17
-rw-r--r--mediagoblin/templates/mediagoblin/utils/prev_next.html13
-rw-r--r--mediagoblin/templates/mediagoblin/utils/profile.html25
-rw-r--r--mediagoblin/tests/test_auth.py9
-rw-r--r--mediagoblin/tests/test_migrations.py403
-rw-r--r--mediagoblin/tests/test_submission.py157
-rwxr-xr-xmediagoblin/tests/test_submission/evilbin0 -> 96284 bytes
-rwxr-xr-xmediagoblin/tests/test_submission/evil.jpgbin0 -> 96284 bytes
-rwxr-xr-xmediagoblin/tests/test_submission/evil.pngbin0 -> 96284 bytes
-rw-r--r--mediagoblin/tests/test_submission/good.jpgbin0 -> 10059 bytes
-rw-r--r--mediagoblin/tests/test_submission/good.pngbin0 -> 50598 bytes
-rw-r--r--mediagoblin/tests/tools.py32
-rw-r--r--mediagoblin/user_pages/__init__.py17
-rw-r--r--mediagoblin/user_pages/forms.py43
-rw-r--r--mediagoblin/user_pages/routing.py3
-rw-r--r--mediagoblin/user_pages/views.py22
-rw-r--r--mediagoblin/util.py45
-rw-r--r--mediagoblin/views.py4
68 files changed, 1783 insertions, 569 deletions
diff --git a/README b/README
index 2ef6f78e..082ab2a7 100644
--- a/README
+++ b/README
@@ -12,7 +12,7 @@ What is GNU MediaGoblin?
* Federated with OStatus!
* Customizable!
* A place for people to collaborate and show off original and derived
- creations. Free, as in freedom. We’re a GNU project in the making,
+ creations. Free, as in freedom. We’re a GNU project in the making,
afterall.
@@ -34,5 +34,5 @@ Where is the documentation?
===========================
Documentation is located in the ``docs/`` directory in a "raw"
-restructured-text form. It is also mirrored at
+restructured-text form. It is also available at
http://docs.mediagoblin.org/ in HTML form.
diff --git a/destroy_environment.py b/destroy_environment.py
deleted file mode 100755
index bbdeffe9..00000000
--- a/destroy_environment.py
+++ /dev/null
@@ -1,22 +0,0 @@
-#!./bin/python
-
-import pymongo
-import sys, os
-
-print "*** WARNING! ***"
-print " Running this will destroy your mediagoblin database,"
-print " remove all your media files in user_dev/, etc."
-
-drop_it = raw_input(
- 'Are you SURE you want to destroy your environment? (if so, type "yes")> ')
-
-if not drop_it == 'yes':
- sys.exit(1)
-
-conn = pymongo.Connection()
-conn.drop_database('mediagoblin')
-
-os.popen('rm -rf user_dev/media')
-os.popen('rm -rf user_dev/beaker')
-
-print "removed all your stuff! okay, now re-run ./bin/buildout"
diff --git a/docs/deploymenthowto.rst b/docs/deploymenthowto.rst
index d943e276..f50edfb6 100644
--- a/docs/deploymenthowto.rst
+++ b/docs/deploymenthowto.rst
@@ -10,4 +10,7 @@ Step 2: ?
Step 3: Write the deployment guide and profit!
-But seriously, this is a stub since we're not quite there, yet.
+But seriously, this is a stub since we're not quite there (yet) but if
+you want to see where we are now, you can try to run the latest
+development version by following the instructions at
+:ref:`hacking-howto`.
diff --git a/docs/git.rst b/docs/git.rst
index 73e7a311..bd0f9d52 100644
--- a/docs/git.rst
+++ b/docs/git.rst
@@ -63,6 +63,10 @@ Further, if you isolate your changes to a branch, then you can work on
multiple issues at the same time and they don't conflict with one
another.
+Name your branches using the isue number and something that makes it clear
+what it's about. For example, if you were working on tagging, you
+might name your branch ``360_tagging``.
+
Properly document your changes
------------------------------
diff --git a/docs/hackinghowto.rst b/docs/hackinghowto.rst
index 914a5135..caafba53 100644
--- a/docs/hackinghowto.rst
+++ b/docs/hackinghowto.rst
@@ -60,7 +60,7 @@ requirements::
On Fedora::
yum install mongodb-server python-paste-deploy python-paste-script \
- git-core python python-devel
+ git-core python python-devel python-lxml
.. YouCanHelp::
@@ -85,7 +85,7 @@ After installing the requirements, follow these steps:
1. Clone the repository::
- git clone http://git.gitorious.org/mediagoblin/mediagoblin.git
+ git clone git://gitorious.org/mediagoblin/mediagoblin.git
2. Bootstrap and run buildout::
@@ -194,7 +194,26 @@ If it's installed, check the mongodb log. On my machine, that's
old lock file: /var/lib/mongodb/mongod.lock. probably means...
-Then delete the lock file and relaunch mongodb.
+in that case you might have had an unclean shutdown. Try::
+
+ sudo mongod --repair
+
+If that didn't work, just delete the lock file and relaunch mongodb.
+
+Anyway, then start the mongodb server in whatever way is appropriate
+for your distro / OS.
+
+
+pkg_resources.DistributionNotFound: distribute
+----------------------------------------------
+
+If you get this while running buildout::
+
+ pkg_resources.DistributionNotFound: distribute
+
+Try this commmand instead::
+
+ python bootstrap.py --distribute && ./bin/buildout
Wiping your user data
diff --git a/lazyserver.sh b/lazyserver.sh
index 4f10f771..e4afdaa5 100755
--- a/lazyserver.sh
+++ b/lazyserver.sh
@@ -18,18 +18,18 @@
if [ "$1" = "-h" ]
then
- echo "$0 [-h] [-c paste.ini] ARGS_to_paster"
- echo " For example:"
- echo " $0 -c fcgi.ini port_number=23371"
- exit 1
+ echo "$0 [-h] [-c paste.ini] ARGS_to_paster"
+ echo " For example:"
+ echo " $0 -c fcgi.ini port_number=23371"
+ exit 1
fi
PASTE_INI=paste.ini
if [ "$1" = "-c" ]
then
- PASTE_INI="$2"
- shift
- shift
+ PASTE_INI="$2"
+ shift
+ shift
fi
if [ -f ./bin/paster ]; then
diff --git a/maketarball.sh b/maketarball.sh
index 2ee78016..5ded9671 100755
--- a/maketarball.sh
+++ b/maketarball.sh
@@ -1,29 +1,48 @@
#!/bin/bash
-# usage: maketarball
-# maketarball <tag>
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
#
-# With no arguments, this creates a source tarball from git master with a
-# filename based on today's date.
+# 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.
#
-# With a <tag> argument, this creates a tarball of the tag.
+# 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/>.
+
+
+# usage: maketarball [-d] <rev-ish>
+#
+# Creates a tarball from a rev-ish. If -d is passed in, then it adds
+# the date to the directory name.
#
# Examples:
#
-# ./maketarball
+# ./maketarball -d master
# ./maketarball v0.0.2
+if [[ -z "$1" ]]; then
+ echo "Usage: $0 [-d] <rev-ish>";
+ exit 1;
+fi
+
NOWDATE=`date "+%Y-%m-%d"`
-if [ -z "$1" ]
-then
- REVISH=master
+if [[ $@ == *-d* ]]; then
+ REVISH=$2
PREFIX="$NOWDATE-$REVISH"
else
REVISH=$1
PREFIX="$REVISH"
fi
+
# convert PREFIX to all lowercase.
# nix the v from tag names.
PREFIX=`echo "$PREFIX" | tr '[A-Z]' '[a-z]' | sed s/v//`
@@ -54,4 +73,4 @@ gzip mediagoblin-$PREFIX.tar
echo "archive at mediagoblin-$PREFIX.tar.gz"
-echo "done." \ No newline at end of file
+echo "done."
diff --git a/mediagoblin.ini b/mediagoblin.ini
index 596107dc..e889646a 100644
--- a/mediagoblin.ini
+++ b/mediagoblin.ini
@@ -8,6 +8,9 @@ email_sender_address = "notice@mediagoblin.example.org"
# set to false to enable sending notices
email_debug_mode = true
+# Set to false to disable registrations
+allow_registration = true
+
## Uncomment this to put some user-overriding templates here
#local_templates = %(here)s/user_dev/templates/
diff --git a/mediagoblin/app.py b/mediagoblin/app.py
index 9454b403..c1ee3d77 100644
--- a/mediagoblin/app.py
+++ b/mediagoblin/app.py
@@ -20,18 +20,12 @@ import urllib
import routes
from webob import Request, exc
-from mediagoblin import routing, util, storage, staticdirect
-from mediagoblin.init.config import (
- read_mediagoblin_config, generate_validation_report)
-from mediagoblin.db.open import setup_connection_and_db_from_config
+from mediagoblin import routing, util
from mediagoblin.mg_globals import setup_globals
from mediagoblin.init.celery import setup_celery_from_config
-from mediagoblin.init import get_jinja_loader
-from mediagoblin.workbench import WorkbenchManager
-
-
-class Error(Exception): pass
-class ImproperlyConfigured(Error): pass
+from mediagoblin.init import (get_jinja_loader, get_staticdirector,
+ setup_global_and_app_config, setup_workbench, setup_database,
+ setup_storage)
class MediaGoblinApp(object):
@@ -55,49 +49,27 @@ class MediaGoblinApp(object):
##############
# Open and setup the config
- global_config, validation_result = read_mediagoblin_config(config_path)
- app_config = global_config['mediagoblin']
- # report errors if necessary
- validation_report = generate_validation_report(
- global_config, validation_result)
- if validation_report:
- raise ImproperlyConfigured(validation_report)
+ global_config, app_config = setup_global_and_app_config(config_path)
##########################################
# Setup other connections / useful objects
##########################################
# Set up the database
- self.connection, self.db = setup_connection_and_db_from_config(
- app_config)
+ self.connection, self.db = setup_database()
# Get the template environment
self.template_loader = get_jinja_loader(
app_config.get('user_template_path'))
# Set up storage systems
- self.public_store = storage.storage_system_from_config(
- app_config, 'publicstore')
- self.queue_store = storage.storage_system_from_config(
- app_config, 'queuestore')
+ self.public_store, self.queue_store = setup_storage()
# set up routing
self.routing = routing.get_mapper()
# set up staticdirector tool
- if app_config.has_key('direct_remote_path'):
- self.staticdirector = staticdirect.RemoteStaticDirect(
- app_config['direct_remote_path'].strip())
- elif app_config.has_key('direct_remote_paths'):
- direct_remote_path_lines = app_config[
- 'direct_remote_paths'].strip().splitlines()
- self.staticdirector = staticdirect.MultiRemoteStaticDirect(
- dict([line.strip().split(' ', 1)
- for line in direct_remote_path_lines]))
- else:
- raise ImproperlyConfigured(
- "One of direct_remote_path or "
- "direct_remote_paths must be provided")
+ self.staticdirector = get_staticdirector(app_config)
# Setup celery, if appropriate
if setup_celery and not app_config.get('celery_setup_elsewhere'):
@@ -116,22 +88,11 @@ class MediaGoblinApp(object):
# object.
#######################################################
- setup_globals(
- app_config=app_config,
- global_config=global_config,
-
- # TODO: No need to set these two up as globals, we could
- # just read them out of mg_globals.app_config
- email_sender_address=app_config['email_sender_address'],
- email_debug_mode=app_config['email_debug_mode'],
-
- # Actual, useful to everyone objects
- app=self,
- db_connection=self.connection,
- database=self.db,
- public_store=self.public_store,
- queue_store=self.queue_store,
- workbench_manager=WorkbenchManager(app_config['workbench_path']))
+ setup_globals(app = self)
+
+ # Workbench *currently* only used by celery, so this only
+ # matters in always eager mode :)
+ setup_workbench()
def __call__(self, environ, start_response):
request = Request(environ)
diff --git a/mediagoblin/auth/lib.py b/mediagoblin/auth/lib.py
index 08bbdd16..6d1aec49 100644
--- a/mediagoblin/auth/lib.py
+++ b/mediagoblin/auth/lib.py
@@ -112,7 +112,7 @@ def send_verification_email(user, request):
# TODO: There is no error handling in place
send_email(
- mg_globals.email_sender_address,
+ mg_globals.app_config['email_sender_address'],
[user['email']],
# TODO
# Due to the distributed nature of GNU MediaGoblin, we should
diff --git a/mediagoblin/auth/routing.py b/mediagoblin/auth/routing.py
index 46c585d2..9547b3ea 100644
--- a/mediagoblin/auth/routing.py
+++ b/mediagoblin/auth/routing.py
@@ -19,18 +19,12 @@ from routes.route import Route
auth_routes = [
Route('mediagoblin.auth.register', '/register/',
controller='mediagoblin.auth.views:register'),
- Route('mediagoblin.auth.register_success', '/register/success/',
- template='mediagoblin/auth/register_success.html',
- controller='mediagoblin.views:simple_template_render'),
Route('mediagoblin.auth.login', '/login/',
controller='mediagoblin.auth.views:login'),
Route('mediagoblin.auth.logout', '/logout/',
controller='mediagoblin.auth.views:logout'),
Route('mediagoblin.auth.verify_email', '/verify_email/',
controller='mediagoblin.auth.views:verify_email'),
- Route('mediagoblin.auth.verify_email_notice', '/verification_required/',
- template='mediagoblin/auth/verification_needed.html',
- controller='mediagoblin.views:simple_template_render'),
Route('mediagoblin.auth.resend_verification', '/resend_verification/',
controller='mediagoblin.auth.views:resend_activation'),
Route('mediagoblin.auth.resend_verification_success',
diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py
index 2450023f..fb5db870 100644
--- a/mediagoblin/auth/views.py
+++ b/mediagoblin/auth/views.py
@@ -19,6 +19,7 @@ import uuid
from webob import exc
from mediagoblin import messages
+from mediagoblin import mg_globals
from mediagoblin.util import render_to_response, redirect
from mediagoblin.db.util import ObjectId
from mediagoblin.auth import lib as auth_lib
@@ -30,6 +31,14 @@ def register(request):
"""
Your classic registration view!
"""
+ # Redirects to indexpage if registrations are disabled
+ if not mg_globals.app_config["allow_registration"]:
+ messages.add_message(
+ request,
+ messages.WARNING,
+ ('Sorry, registration is disabled on this instance.'))
+ return redirect(request, "index")
+
register_form = auth_forms.RegistrationForm(request.POST)
if request.method == 'POST' and register_form.validate():
@@ -46,16 +55,25 @@ def register(request):
else:
# Create the user
- entry = request.db.User()
- entry['username'] = request.POST['username'].lower()
- entry['email'] = request.POST['email']
- entry['pw_hash'] = auth_lib.bcrypt_gen_password_hash(
+ user = request.db.User()
+ user['username'] = request.POST['username'].lower()
+ user['email'] = request.POST['email']
+ user['pw_hash'] = auth_lib.bcrypt_gen_password_hash(
request.POST['password'])
- entry.save(validate=True)
-
- send_verification_email(entry, request)
+ user.save(validate=True)
+
+ # log the user in
+ request.session['user_id'] = unicode(user['_id'])
+ request.session.save()
- return redirect(request, "mediagoblin.auth.register_success")
+ # send verification email
+ send_verification_email(user, request)
+
+ # redirect the user to their homepage... there will be a
+ # message waiting for them to verify their email
+ return redirect(
+ request, 'mediagoblin.user_pages.user_home',
+ user=user['username'])
return render_to_response(
request,
@@ -98,13 +116,14 @@ def login(request):
'mediagoblin/auth/login.html',
{'login_form': login_form,
'next': request.GET.get('next') or request.POST.get('next'),
- 'login_failed': login_failed})
+ 'login_failed': login_failed,
+ 'allow_registration': mg_globals.app_config["allow_registration"]})
def logout(request):
# Maybe deleting the user_id parameter would be enough?
request.session.delete()
-
+
return redirect(request, "index")
@@ -128,16 +147,16 @@ def verify_email(request):
user.save()
verification_successful = True
messages.add_message(
- request,
- messages.SUCCESS,
+ request,
+ messages.SUCCESS,
('Your email address has been verified. '
'You may now login, edit your profile, and submit images!'))
else:
verification_successful = False
- messages.add_message(request,
- messages.ERROR,
- 'The verification key or user id is incorrect')
-
+ messages.add_message(request,
+ messages.ERROR,
+ 'The verification key or user id is incorrect')
+
return render_to_response(
request,
'mediagoblin/user_pages/user.html',
@@ -156,4 +175,10 @@ def resend_activation(request):
send_verification_email(request.user, request)
- return redirect(request, 'mediagoblin.auth.resend_verification_success')
+ messages.add_message(
+ request,
+ messages.INFO,
+ 'Resent your verification email.')
+ return redirect(
+ request, 'mediagoblin.user_pages.user_home',
+ user=request.user['username'])
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index 5aae6439..a296f0c1 100644
--- a/mediagoblin/config_spec.ini
+++ b/mediagoblin/config_spec.ini
@@ -1,7 +1,7 @@
[mediagoblin]
# database stuff
db_host = string()
-db_name = string()
+db_name = string(default="mediagoblin")
db_port = integer()
#
@@ -21,6 +21,9 @@ direct_remote_path = string(default="/mgoblin_static/")
email_debug_mode = boolean(default=True)
email_sender_address = string(default="notice@mediagoblin.example.org")
+# Set to false to disable registrations
+allow_registration = boolean(default=True)
+
# tag parsing
tags_delimiter = string(default=",")
tags_case_sensitive = boolean(default=False)
@@ -78,4 +81,4 @@ celeryd_eta_scheduler_precision = float()
# known lists
celery_routes = string_list()
-celery_imports = string_list() \ No newline at end of file
+celery_imports = string_list()
diff --git a/mediagoblin/db/indexes.py b/mediagoblin/db/indexes.py
index d0e11311..30d43c98 100644
--- a/mediagoblin/db/indexes.py
+++ b/mediagoblin/db/indexes.py
@@ -45,11 +45,13 @@ REQUIRED READING:
To remove deprecated indexes
----------------------------
-Removing deprecated indexes is easier, just do:
+Removing deprecated indexes is the same, just move the index into the
+deprecated indexes mapping.
-INACTIVE_INDEXES = {
- 'collection_name': [
- 'deprecated_index_identifier1', 'deprecated_index_identifier2']}
+DEPRECATED_INDEXES = {
+ 'collection_name': {
+ 'deprecated_index_identifier1': {
+ 'index': [index_foo_goes_here]}}
... etc.
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index 712f8ab4..6a8ebcf9 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -14,56 +14,41 @@
# 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.db.util import RegisterMigration
from mediagoblin.util import cleaned_markdown_conversion
-from mongokit import DocumentMigration
+# Please see mediagoblin/tests/test_migrations.py for some examples of
+# basic migrations.
-class MediaEntryMigration(DocumentMigration):
- def allmigration01_uploader_to_reference(self):
- """
- Old MediaEntry['uploader'] accidentally embedded the User instead
- of referencing it. Fix that!
- """
- # uploader is an associative array
- self.target = {'uploader': {'$type': 3}}
- if not self.status:
- for doc in self.collection.find(self.target):
- self.update = {
- '$set': {
- 'uploader': doc['uploader']['_id']}}
- self.collection.update(
- self.target, self.update, multi=True, safe=True)
- def allmigration02_add_description_html(self):
- """
- Now that we can have rich descriptions via Markdown, we should
- update all existing entries to record the rich description versions.
- """
- self.target = {'description_html': {'$exists': False},
- 'description': {'$exists': True}}
+@RegisterMigration(1)
+def user_add_bio_html(database):
+ """
+ Users now have richtext bios via Markdown, reflect appropriately.
+ """
+ collection = database['users']
- if not self.status:
- for doc in self.collection.find(self.target):
- self.update = {
- '$set': {
- 'description_html': cleaned_markdown_conversion(
- doc['description'])}}
-
-class UserMigration(DocumentMigration):
- def allmigration01_add_bio_and_url_profile(self):
- """
- User can elaborate profile with home page and biography
- """
- self.target = {'url': {'$exists': False},
- 'bio': {'$exists': False}}
- if not self.status:
- for doc in self.collection.find(self.target):
- self.update = {
- '$set': {'url': '',
- 'bio': ''}}
- self.collection.update(
- self.target, self.update, multi=True, safe=True)
-
-
-MIGRATE_CLASSES = ['MediaEntry', 'User']
+ target = collection.find(
+ {'bio_html': {'$exists': False}})
+
+ for document in target:
+ document['bio_html'] = cleaned_markdown_conversion(
+ document['bio'])
+ collection.save(document)
+
+
+@RegisterMigration(2)
+def mediaentry_mediafiles_main_to_original(database):
+ """
+ Rename "main" media file to "original".
+ """
+ collection = database['media_entries']
+ target = collection.find(
+ {'media_files.main': {'$exists': True}})
+
+ for document in target:
+ original = document['media_files'].pop('main')
+ document['media_files']['original'] = original
+
+ collection.save(document)
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index 8fcbb208..4ef2d928 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -16,13 +16,16 @@
import datetime, uuid
-from mongokit import Document, Set
+from mongokit import Document
from mediagoblin import util
from mediagoblin.auth import lib as auth_lib
from mediagoblin import mg_globals
from mediagoblin.db import migrations
from mediagoblin.db.util import ASCENDING, DESCENDING, ObjectId
+from mediagoblin.util import Pagination
+from mediagoblin.util import DISPLAY_IMAGE_FETCHING_ORDER
+
###################
# Custom validators
@@ -34,6 +37,32 @@ from mediagoblin.db.util import ASCENDING, DESCENDING, ObjectId
class User(Document):
+ """
+ A user of MediaGoblin.
+
+ Structure:
+ - username: The username of this user, should be unique to this instance.
+ - email: Email address of this user
+ - created: When the user was created
+ - plugin_data: a mapping of extra plugin information for this User.
+ Nothing uses this yet as we don't have plugins, but someday we
+ might... :)
+ - pw_hash: Hashed version of user's password.
+ - email_verified: Whether or not the user has verified their email or not.
+ Most parts of the site are disabled for users who haven't yet.
+ - status: whether or not the user is active, etc. Currently only has two
+ values, 'needs_email_verification' or 'active'. (In the future, maybe
+ we'll change this to a boolean with a key of 'active' and have a
+ separate field for a reason the user's been disabled if that's
+ appropriate... email_verified is already separate, after all.)
+ - verification_key: If the user is awaiting email verification, the user
+ will have to provide this key (which will be encoded in the presented
+ URL) in order to confirm their email as active.
+ - is_admin: Whether or not this user is an administrator or not.
+ - url: this user's personal webpage/website, if appropriate.
+ - bio: biography of this user (plaintext, in markdown)
+ - bio_html: biography of the user converted to proper HTML.
+ """
__collection__ = 'users'
structure = {
@@ -47,7 +76,8 @@ class User(Document):
'verification_key': unicode,
'is_admin': bool,
'url' : unicode,
- 'bio' : unicode
+ 'bio' : unicode, # May contain markdown
+ 'bio_html': unicode, # May contain plaintext, or HTML
}
required_fields = ['username', 'created', 'pw_hash', 'email']
@@ -58,8 +88,6 @@ class User(Document):
'status': u'needs_email_verification',
'verification_key': lambda: unicode(uuid.uuid4()),
'is_admin': False}
-
- migration_handler = migrations.UserMigration
def check_login(self, password):
"""
@@ -70,6 +98,80 @@ class User(Document):
class MediaEntry(Document):
+ """
+ Record of a piece of media.
+
+ Structure:
+ - uploader: A reference to a User who uploaded this.
+
+ - title: Title of this work
+
+ - slug: A normalized "slug" which can be used as part of a URL to retrieve
+ this work, such as 'my-works-name-in-slug-form' may be viewable by
+ 'http://mg.example.org/u/username/m/my-works-name-in-slug-form/'
+ Note that since URLs are constructed this way, slugs must be unique
+ per-uploader. (An index is provided to enforce that but code should be
+ written on the python side to ensure this as well.)
+
+ - created: Date and time of when this piece of work was uploaded.
+
+ - description: Uploader-set description of this work. This can be marked
+ up with MarkDown for slight fanciness (links, boldness, italics,
+ paragraphs...)
+
+ - description_html: Rendered version of the description, run through
+ Markdown and cleaned with our cleaning tool.
+
+ - media_type: What type of media is this? Currently we only support
+ 'image' ;)
+
+ - media_data: Extra information that's media-format-dependent.
+ For example, images might contain some EXIF data that's not appropriate
+ to other formats. You might store it like:
+
+ mediaentry['media_data']['exif'] = {
+ 'manufacturer': 'CASIO',
+ 'model': 'QV-4000',
+ 'exposure_time': .659}
+
+ Alternately for video you might store:
+
+ # play length in seconds
+ mediaentry['media_data']['play_length'] = 340
+
+ ... so what's appropriate here really depends on the media type.
+
+ - plugin_data: a mapping of extra plugin information for this User.
+ Nothing uses this yet as we don't have plugins, but someday we
+ might... :)
+
+ - tags: A list of tags. Each tag is stored as a dictionary that has a key
+ for the actual name and the normalized name-as-slug, so ultimately this
+ looks like:
+ [{'name': 'Gully Gardens',
+ 'slug': 'gully-gardens'},
+ {'name': 'Castle Adventure Time?!",
+ 'slug': 'castle-adventure-time'}]
+
+ - state: What's the state of this file? Active, inactive, disabled, etc...
+ But really for now there are only two states:
+ "unprocessed": uploaded but needs to go through processing for display
+ "processed": processed and able to be displayed
+
+ - queued_media_file: storage interface style filepath describing a file
+ queued for processing. This is stored in the mg_globals.queue_store
+ storage system.
+
+ - media_files: Files relevant to this that have actually been processed
+ and are available for various types of display. Stored like:
+ {'thumb': ['dir1', 'dir2', 'pic.png'}
+
+ - attachment_files: A list of "attachment" files, ones that aren't
+ critical to this piece of media but may be usefully relevant to people
+ viewing the work. (currently unused.)
+
+ - thumbnail_file: Deprecated... we should remove this ;)
+ """
__collection__ = 'media_entries'
structure = {
@@ -106,12 +208,28 @@ class MediaEntry(Document):
'created': datetime.datetime.utcnow,
'state': u'unprocessed'}
- migration_handler = migrations.MediaEntryMigration
-
def get_comments(self):
return self.db.MediaComment.find({
'media_entry': self['_id']}).sort('created', DESCENDING)
+ def get_display_media(self, media_map, fetch_order=DISPLAY_IMAGE_FETCHING_ORDER):
+ """
+ Find the best media for display.
+
+ Args:
+ - media_map: a dict like
+ {u'image_size': [u'dir1', u'dir2', u'image.jpg']}
+ - fetch_order: the order we should try fetching images in
+
+ Returns:
+ (media_size, media_path)
+ """
+ media_sizes = media_map.keys()
+
+ for media_size in DISPLAY_IMAGE_FETCHING_ORDER:
+ if media_size in media_sizes:
+ return media_map[media_size]
+
def main_mediafile(self):
pass
@@ -120,7 +238,7 @@ class MediaEntry(Document):
duplicate = mg_globals.database.media_entries.find_one(
{'slug': self['slug']})
-
+
if duplicate:
self['slug'] = "%s-%s" % (self['_id'], self['slug'])
@@ -142,12 +260,12 @@ class MediaEntry(Document):
'mediagoblin.user_pages.media_home',
user=uploader['username'],
media=unicode(self['_id']))
-
+
def url_to_prev(self, urlgen):
"""
Provide a url to the previous entry from this user, if there is one
"""
- cursor = self.db.MediaEntry.find({'_id' : {"$gt": self['_id']},
+ cursor = self.db.MediaEntry.find({'_id' : {"$gt": self['_id']},
'uploader': self['uploader'],
'state': 'processed'}).sort(
'_id', ASCENDING).limit(1)
@@ -155,12 +273,12 @@ class MediaEntry(Document):
return urlgen('mediagoblin.user_pages.media_home',
user=self.uploader()['username'],
media=unicode(cursor[0]['slug']))
-
+
def url_to_next(self, urlgen):
"""
Provide a url to the next entry from this user, if there is one
"""
- cursor = self.db.MediaEntry.find({'_id' : {"$lt": self['_id']},
+ cursor = self.db.MediaEntry.find({'_id' : {"$lt": self['_id']},
'uploader': self['uploader'],
'state': 'processed'}).sort(
'_id', DESCENDING).limit(1)
@@ -175,6 +293,18 @@ class MediaEntry(Document):
class MediaComment(Document):
+ """
+ A comment on a MediaEntry.
+
+ Structure:
+ - media_entry: The media entry this comment is attached to
+ - author: user who posted this comment
+ - created: when the comment was created
+ - content: plaintext (but markdown'able) version of the comment's content.
+ - content_html: the actual html-rendered version of the comment displayed.
+ Run through Markdown and the HTML cleaner.
+ """
+
__collection__ = 'media_comments'
structure = {
@@ -196,6 +326,7 @@ class MediaComment(Document):
def author(self):
return self.db.User.find_one({'_id': self['author']})
+
REGISTER_MODELS = [
MediaEntry,
User,
diff --git a/mediagoblin/db/open.py b/mediagoblin/db/open.py
index cae33394..e5fde6f9 100644
--- a/mediagoblin/db/open.py
+++ b/mediagoblin/db/open.py
@@ -14,24 +14,42 @@
# 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 pymongo
import mongokit
from paste.deploy.converters import asint
from mediagoblin.db import models
-def connect_database_from_config(app_config):
- """Connect to the main database, take config from app_config"""
+def connect_database_from_config(app_config, use_pymongo=False):
+ """
+ Connect to the main database, take config from app_config
+
+ Optionally use pymongo instead of mongokit for the connection.
+ """
port = app_config.get('db_port')
if port:
port = asint(port)
- connection = mongokit.Connection(
- app_config.get('db_host'), port)
+
+ if use_pymongo:
+ connection = pymongo.Connection(
+ app_config.get('db_host'), port)
+ else:
+ connection = mongokit.Connection(
+ app_config.get('db_host'), port)
return connection
-def setup_connection_and_db_from_config(app_config):
- connection = connect_database_from_config(app_config)
- database_path = app_config.get('db_name', 'mediagoblin')
+
+def setup_connection_and_db_from_config(app_config, use_pymongo=False):
+ """
+ Setup connection and database from config.
+
+ Optionally use pymongo instead of mongokit.
+ """
+ connection = connect_database_from_config(app_config, use_pymongo)
+ database_path = app_config['db_name']
db = connection[database_path]
- models.register_models(connection)
- # Could configure indexes here on db
+
+ if not use_pymongo:
+ models.register_models(connection)
+
return (connection, db)
diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py
index 70c37945..0f3220d2 100644
--- a/mediagoblin/db/util.py
+++ b/mediagoblin/db/util.py
@@ -37,6 +37,11 @@ from mongokit import ObjectId
from mediagoblin.db.indexes import ACTIVE_INDEXES, DEPRECATED_INDEXES
+################
+# Indexing tools
+################
+
+
def add_new_indexes(database, active_indexes=ACTIVE_INDEXES):
"""
Add any new indexes to the database.
@@ -81,21 +86,206 @@ def remove_deprecated_indexes(database, deprecated_indexes=DEPRECATED_INDEXES):
Args:
- database: pymongo or mongokit database instance.
- deprecated_indexes: the indexes to deprecate in the pattern of:
- {'collection': ['index_identifier1', 'index_identifier2']}
+ {'collection_name': {
+ 'identifier': {
+ 'index': [index_foo_goes_here],
+ 'unique': True}}
+
+ (... although we really only need the 'identifier' here, as the
+ rest of the information isn't used in this case. But it's kept
+ around so we can remember what it was)
Returns:
A list of indexes removed in form ('collection', 'index_name')
"""
indexes_removed = []
- for collection_name, index_names in deprecated_indexes.iteritems():
+ for collection_name, indexes in deprecated_indexes.iteritems():
collection = database[collection_name]
collection_indexes = collection.index_information().keys()
- for index_name in index_names:
+ for index_name, index_data in indexes.iteritems():
if index_name in collection_indexes:
collection.drop_index(index_name)
indexes_removed.append((collection_name, index_name))
return indexes_removed
+
+
+#################
+# Migration tools
+#################
+
+# The default migration registry...
+#
+# Don't set this yourself! RegisterMigration will automatically fill
+# this with stuff via decorating methods in migrations.py
+
+class MissingCurrentMigration(Exception): pass
+
+
+MIGRATIONS = {}
+
+
+class RegisterMigration(object):
+ """
+ Tool for registering migrations
+
+ Call like:
+
+ @RegisterMigration(33)
+ def update_dwarves(database):
+ [...]
+
+ This will register your migration with the default migration
+ registry. Alternately, to specify a very specific
+ migration_registry, you can pass in that as the second argument.
+
+ Note, the number of your migration should NEVER be 0 or less than
+ 0. 0 is the default "no migrations" state!
+ """
+ def __init__(self, migration_number, migration_registry=MIGRATIONS):
+ assert migration_number > 0, "Migration number must be > 0!"
+ assert not migration_registry.has_key(migration_number), \
+ "Duplicate migration numbers detected! That's not allowed!"
+
+ self.migration_number = migration_number
+ self.migration_registry = migration_registry
+
+ def __call__(self, migration):
+ self.migration_registry[self.migration_number] = migration
+ return migration
+
+
+class MigrationManager(object):
+ """
+ Migration handling tool.
+
+ Takes information about a database, lets you update the database
+ to the latest migrations, etc.
+ """
+ def __init__(self, database, migration_registry=MIGRATIONS):
+ """
+ Args:
+ - database: database we're going to migrate
+ - migration_registry: where we should find all migrations to
+ run
+ """
+ self.database = database
+ self.migration_registry = migration_registry
+ self._sorted_migrations = None
+
+ def _ensure_current_migration_record(self):
+ """
+ If there isn't a database[u'app_metadata'] mediagoblin entry
+ with the 'current_migration', throw an error.
+ """
+ if self.database_current_migration() is None:
+ raise MissingCurrentMigration(
+ "Tried to call function which requires "
+ "'current_migration' set in database")
+
+ @property
+ def sorted_migrations(self):
+ """
+ Sort migrations if necessary and store in self._sorted_migrations
+ """
+ if not self._sorted_migrations:
+ self._sorted_migrations = sorted(
+ self.migration_registry.items(),
+ # sort on the key... the migration number
+ key=lambda migration_tuple: migration_tuple[0])
+
+ return self._sorted_migrations
+
+ def latest_migration(self):
+ """
+ Return a migration number for the latest migration, or 0 if
+ there are no migrations.
+ """
+ if self.sorted_migrations:
+ return self.sorted_migrations[-1][0]
+ else:
+ # If no migrations have been set, we start at 0.
+ return 0
+
+ def set_current_migration(self, migration_number):
+ """
+ Set the migration in the database to migration_number
+ """
+ # Add the mediagoblin migration if necessary
+ self.database[u'app_metadata'].update(
+ {u'_id': u'mediagoblin'},
+ {u'$set': {u'current_migration': migration_number}},
+ upsert=True)
+
+ def install_migration_version_if_missing(self):
+ """
+ Sets the migration to the latest version if no migration
+ version at all is set.
+ """
+ mgoblin_metadata = self.database[u'app_metadata'].find_one(
+ {u'_id': u'mediagoblin'})
+ if not mgoblin_metadata:
+ latest_migration = self.latest_migration()
+ self.set_current_migration(latest_migration)
+
+ def database_current_migration(self):
+ """
+ Return the current migration in the database.
+ """
+ mgoblin_metadata = self.database[u'app_metadata'].find_one(
+ {u'_id': u'mediagoblin'})
+ if not mgoblin_metadata:
+ return None
+ else:
+ return mgoblin_metadata[u'current_migration']
+
+ def database_at_latest_migration(self):
+ """
+ See if the database is at the latest migration.
+ Returns a boolean.
+ """
+ current_migration = self.database_current_migration()
+ return current_migration == self.latest_migration()
+
+ def migrations_to_run(self):
+ """
+ Get a list of migrations to run still, if any.
+
+ Note that calling this will set your migration version to the
+ latest version if it isn't installed to anything yet!
+ """
+ self._ensure_current_migration_record()
+
+ db_current_migration = self.database_current_migration()
+
+ return [
+ (migration_number, migration_func)
+ for migration_number, migration_func in self.sorted_migrations
+ if migration_number > db_current_migration]
+
+ def migrate_new(self, pre_callback=None, post_callback=None):
+ """
+ Run all migrations.
+
+ Includes two optional args:
+ - pre_callback: if called, this is a callback on something to
+ run pre-migration. Takes (migration_number, migration_func)
+ as arguments
+ - pre_callback: if called, this is a callback on something to
+ run post-migration. Takes (migration_number, migration_func)
+ as arguments
+ """
+ # If we aren't set to any version number, presume we're at the
+ # latest (which means we'll do nothing here...)
+ self.install_migration_version_if_missing()
+
+ for migration_number, migration_func in self.migrations_to_run():
+ if pre_callback:
+ pre_callback(migration_number, migration_func)
+ migration_func(self.database)
+ self.set_current_migration(migration_number)
+ if post_callback:
+ post_callback(migration_number, migration_func)
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index 081eda62..2e90274e 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -38,8 +38,9 @@ def require_active_login(controller):
def new_controller_func(request, *args, **kwargs):
if request.user and \
request.user.get('status') == u'needs_email_verification':
- return redirect(request,
- 'mediagoblin.auth.verify_email_notice')
+ return redirect(
+ request, 'mediagoblin.user_pages.user_home',
+ user=request.user['username'])
elif not request.user or request.user.get('status') != u'active':
return exc.HTTPFound(
location="%s?next=%s" % (
diff --git a/mediagoblin/edit/__init__.py b/mediagoblin/edit/__init__.py
index e69de29b..a8eeb5ed 100644
--- a/mediagoblin/edit/__init__.py
+++ b/mediagoblin/edit/__init__.py
@@ -0,0 +1,17 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py
index e7a86bba..a1783a72 100644
--- a/mediagoblin/edit/forms.py
+++ b/mediagoblin/edit/forms.py
@@ -24,7 +24,8 @@ class EditForm(wtforms.Form):
'Title',
[wtforms.validators.Length(min=0, max=500)])
slug = wtforms.TextField(
- 'Slug')
+ 'Slug',
+ [wtforms.validators.Required(message="The slug can't be empty")])
description = wtforms.TextAreaField('Description of this work')
tags = wtforms.TextField(
'Tags',
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index e4ebe8d7..5cbaadb5 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -22,13 +22,11 @@ from mediagoblin import messages
from mediagoblin import mg_globals
from mediagoblin.util import (
render_to_response, redirect, clean_html, convert_to_tag_list_of_dicts,
- media_tags_as_string)
+ media_tags_as_string, cleaned_markdown_conversion)
from mediagoblin.edit import forms
from mediagoblin.edit.lib import may_edit_media
from mediagoblin.decorators import require_active_login, get_user_media_entry
-import markdown
-
@get_user_media_entry
@require_active_login
@@ -59,11 +57,8 @@ def edit_media(request, media):
media['tags'] = convert_to_tag_list_of_dicts(
request.POST.get('tags'))
- md = markdown.Markdown(
- safe_mode = 'escape')
- media['description_html'] = clean_html(
- md.convert(
- media['description']))
+ media['description_html'] = cleaned_markdown_conversion(
+ media['description'])
media['slug'] = request.POST['slug']
media.save()
@@ -108,6 +103,9 @@ def edit_profile(request):
if request.method == 'POST' and form.validate():
user['url'] = request.POST['url']
user['bio'] = request.POST['bio']
+
+ user['bio_html'] = cleaned_markdown_conversion(user['bio'])
+
user.save()
messages.add_message(request,
@@ -115,7 +113,7 @@ def edit_profile(request):
'Profile edited!')
return redirect(request,
'mediagoblin.user_pages.user_home',
- username=edit_username)
+ user=edit_username)
return render_to_response(
request,
diff --git a/mediagoblin/gmg_commands/__init__.py b/mediagoblin/gmg_commands/__init__.py
index 0cb4d3a2..921f0430 100644
--- a/mediagoblin/gmg_commands/__init__.py
+++ b/mediagoblin/gmg_commands/__init__.py
@@ -40,6 +40,10 @@ SUBCOMMAND_MAP = {
'setup': 'mediagoblin.gmg_commands.users:changepw_parser_setup',
'func': 'mediagoblin.gmg_commands.users:changepw',
'help': 'Makes admin an user'},
+ 'wipealldata': {
+ 'setup': 'mediagoblin.gmg_commands.wipealldata:wipe_parser_setup',
+ 'func': 'mediagoblin.gmg_commands.wipealldata:wipe',
+ 'help': 'Wipes **all** the data for this MediaGoblin instance'},
}
diff --git a/mediagoblin/gmg_commands/migrate.py b/mediagoblin/gmg_commands/migrate.py
index ab1a267b..94adc9e0 100644
--- a/mediagoblin/gmg_commands/migrate.py
+++ b/mediagoblin/gmg_commands/migrate.py
@@ -14,10 +14,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import sys
-from mediagoblin.db import migrations
from mediagoblin.db import util as db_util
-from mediagoblin.gmg_commands import util as commands_util
+from mediagoblin.db.open import setup_connection_and_db_from_config
+from mediagoblin.init.config import read_mediagoblin_config
+
+# This MUST be imported so as to set up the appropriate migrations!
+from mediagoblin.db import migrations
def migrate_parser_setup(subparser):
@@ -26,31 +30,41 @@ def migrate_parser_setup(subparser):
help="Config file used to set up environment")
+def _print_started_migration(migration_number, migration_func):
+ sys.stdout.write(
+ "Running migration %s, '%s'... " % (
+ migration_number, migration_func.func_name))
+ sys.stdout.flush()
+
+
+def _print_finished_migration(migration_number, migration_func):
+ sys.stdout.write("done.\n")
+ sys.stdout.flush()
+
+
def migrate(args):
- mgoblin_app = commands_util.setup_app(args)
+ config, validation_result = read_mediagoblin_config(args.conf_file)
+ connection, db = setup_connection_and_db_from_config(
+ config['mediagoblin'], use_pymongo=True)
+ migration_manager = db_util.MigrationManager(db)
# Clear old indexes
print "== Clearing old indexes... =="
- removed_indexes = db_util.remove_deprecated_indexes(mgoblin_app.db)
+ removed_indexes = db_util.remove_deprecated_indexes(db)
for collection, index_name in removed_indexes:
print "Removed index '%s' in collection '%s'" % (
index_name, collection)
# Migrate
- print "== Applying migrations... =="
- for model_name in migrations.MIGRATE_CLASSES:
- model = getattr(mgoblin_app.db, model_name)
-
- if not hasattr(model, 'migration_handler') or not model.collection:
- continue
-
- migration = model.migration_handler(model)
- migration.migrate_all(collection=model.collection)
+ print "\n== Applying migrations... =="
+ migration_manager.migrate_new(
+ pre_callback=_print_started_migration,
+ post_callback=_print_finished_migration)
# Add new indexes
- print "== Adding new indexes... =="
- new_indexes = db_util.add_new_indexes(mgoblin_app.db)
+ print "\n== Adding new indexes... =="
+ new_indexes = db_util.add_new_indexes(db)
for collection, index_name in new_indexes:
print "Added index '%s' to collection '%s'" % (
diff --git a/mediagoblin/gmg_commands/users.py b/mediagoblin/gmg_commands/users.py
index b4a6bbc1..14b6875d 100644
--- a/mediagoblin/gmg_commands/users.py
+++ b/mediagoblin/gmg_commands/users.py
@@ -1,3 +1,19 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
from mediagoblin.gmg_commands import util as commands_util
from mediagoblin.auth import lib as auth_lib
from mediagoblin import mg_globals
diff --git a/mediagoblin/gmg_commands/wipealldata.py b/mediagoblin/gmg_commands/wipealldata.py
new file mode 100644
index 00000000..9ad32051
--- /dev/null
+++ b/mediagoblin/gmg_commands/wipealldata.py
@@ -0,0 +1,51 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import pymongo
+import sys
+import os
+import shutil
+
+
+def wipe_parser_setup(subparser):
+ pass
+
+
+def wipe(args):
+ print "*** WARNING! ***"
+ print ""
+ print "Running this will destroy your mediagoblin database,"
+ print "remove all your media files in user_dev/, etc."
+
+ drop_it = raw_input(
+ 'Are you **SURE** you want to destroy your environment? '
+ '(if so, type "yes")> ')
+
+ if not drop_it == 'yes':
+ return
+
+ print "nixing data in mongodb...."
+ conn = pymongo.Connection()
+ conn.drop_database('mediagoblin')
+
+ for directory in [os.path.join(os.getcwd(), "user_dev", "media"),
+ os.path.join(os.getcwd(), "user_dev", "beaker")]:
+ if os.path.exists(directory):
+ print "nixing %s...." % directory
+ shutil.rmtree(directory)
+
+ print "removed all your stuff! okay, now re-run ./bin/buildout"
diff --git a/mediagoblin/init/__init__.py b/mediagoblin/init/__init__.py
index b8ed2456..ff005703 100644
--- a/mediagoblin/init/__init__.py
+++ b/mediagoblin/init/__init__.py
@@ -15,6 +15,68 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import jinja2
+from mediagoblin import staticdirect
+from mediagoblin.init.config import (
+ read_mediagoblin_config, generate_validation_report)
+from mediagoblin import mg_globals
+from mediagoblin.mg_globals import setup_globals
+from mediagoblin.db.open import setup_connection_and_db_from_config
+from mediagoblin.db.util import MigrationManager
+from mediagoblin.workbench import WorkbenchManager
+from mediagoblin.storage import storage_system_from_config
+
+
+class Error(Exception): pass
+class ImproperlyConfigured(Error): pass
+
+
+def setup_global_and_app_config(config_path):
+ global_config, validation_result = read_mediagoblin_config(config_path)
+ app_config = global_config['mediagoblin']
+ # report errors if necessary
+ validation_report = generate_validation_report(
+ global_config, validation_result)
+ if validation_report:
+ raise ImproperlyConfigured(validation_report)
+
+ setup_globals(
+ app_config=app_config,
+ global_config=global_config)
+
+ return global_config, app_config
+
+
+def setup_database():
+ app_config = mg_globals.app_config
+
+ # This MUST be imported so as to set up the appropriate migrations!
+ from mediagoblin.db import migrations
+
+ # Set up the database
+ connection, db = setup_connection_and_db_from_config(app_config)
+
+ # Init the migration number if necessary
+ migration_manager = MigrationManager(db)
+ migration_manager.install_migration_version_if_missing()
+
+ # Tiny hack to warn user if our migration is out of date
+ if not migration_manager.database_at_latest_migration():
+ db_migration_num = migration_manager.database_current_migration()
+ latest_migration_num = migration_manager.latest_migration()
+ if db_migration_num < latest_migration_num:
+ print (
+ "*WARNING:* Your migrations are out of date, "
+ "maybe run ./bin/gmg migrate?")
+ elif db_migration_num > latest_migration_num:
+ print (
+ "*WARNING:* Your migrations are out of date... "
+ "in fact they appear to be from the future?!")
+
+ setup_globals(
+ db_connection = connection,
+ database = db)
+
+ return connection, db
def get_jinja_loader(user_template_path=None):
@@ -31,3 +93,40 @@ def get_jinja_loader(user_template_path=None):
jinja2.PackageLoader('mediagoblin', 'templates')])
else:
return jinja2.PackageLoader('mediagoblin', 'templates')
+
+
+def get_staticdirector(app_config):
+ if app_config.has_key('direct_remote_path'):
+ return staticdirect.RemoteStaticDirect(
+ app_config['direct_remote_path'].strip())
+ elif app_config.has_key('direct_remote_paths'):
+ direct_remote_path_lines = app_config[
+ 'direct_remote_paths'].strip().splitlines()
+ return staticdirect.MultiRemoteStaticDirect(
+ dict([line.strip().split(' ', 1)
+ for line in direct_remote_path_lines]))
+ else:
+ raise ImproperlyConfigured(
+ "One of direct_remote_path or "
+ "direct_remote_paths must be provided")
+
+
+def setup_storage():
+ app_config = mg_globals.app_config
+
+ public_store = storage_system_from_config(app_config, 'publicstore')
+ queue_store = storage_system_from_config(app_config, 'queuestore')
+
+ setup_globals(
+ public_store = public_store,
+ queue_store = queue_store)
+
+ return public_store, queue_store
+
+
+def setup_workbench():
+ app_config = mg_globals.app_config
+
+ workbench_manager = WorkbenchManager(app_config['workbench_path'])
+
+ setup_globals(workbench_manager = workbench_manager)
diff --git a/mediagoblin/init/celery/__init__.py b/mediagoblin/init/celery/__init__.py
index 67c3dfa0..bfae954e 100644
--- a/mediagoblin/init/celery/__init__.py
+++ b/mediagoblin/init/celery/__init__.py
@@ -62,7 +62,7 @@ def setup_celery_from_config(app_config, global_config,
celery_mongo_settings['port'] = app_config['db_port']
if celery_settings['BROKER_BACKEND'] == 'mongodb':
celery_settings['BROKER_PORT'] = app_config['db_port']
- celery_mongo_settings['database'] = app_config.get('db_name', 'mediagoblin')
+ celery_mongo_settings['database'] = app_config['db_name']
celery_settings['CELERY_MONGODB_BACKEND_SETTINGS'] = celery_mongo_settings
diff --git a/mediagoblin/mg_globals.py b/mediagoblin/mg_globals.py
index 739f44ee..80ff5ead 100644
--- a/mediagoblin/mg_globals.py
+++ b/mediagoblin/mg_globals.py
@@ -1,3 +1,18 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
In some places, we need to access the database, public_store, queue_store
"""
@@ -20,12 +35,6 @@ database = None
public_store = None
queue_store = None
-# Dump mail to stdout instead of sending it:
-email_debug_mode = False
-
-# Address for sending out mails
-email_sender_address = None
-
# A WorkBenchManager
workbench_manager = None
diff --git a/mediagoblin/process_media/__init__.py b/mediagoblin/process_media/__init__.py
index da3e887e..125b24e0 100644
--- a/mediagoblin/process_media/__init__.py
+++ b/mediagoblin/process_media/__init__.py
@@ -57,36 +57,43 @@ def process_media_initial(media_id):
thumb.save(thumb_file, "JPEG", quality=90)
"""
- Create medium file, used in `media.html`
+ If the size of the original file exceeds the specified size of a `medium`
+ file, a `medium.jpg` files is created and later associated with the media
+ entry.
"""
medium = Image.open(queued_filename)
- medium.thumbnail(MEDIUM_SIZE, Image.ANTIALIAS)
+ medium_processed = False
- if medium.mode != "RGB":
- medium = medium.convert("RGB")
+ if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1]:
+ medium.thumbnail(MEDIUM_SIZE, Image.ANTIALIAS)
- medium_filepath = create_pub_filepath(entry, 'medium.jpg')
+ if medium.mode != "RGB":
+ medium = medium.convert("RGB")
- medium_file = mgg.public_store.get_file(medium_filepath, 'w')
- with medium_file:
- medium.save(medium_file, "JPEG", quality=90)
+ medium_filepath = create_pub_filepath(entry, 'medium.jpg')
+
+ medium_file = mgg.public_store.get_file(medium_filepath, 'w')
+ with medium_file:
+ medium.save(medium_file, "JPEG", quality=90)
+ medium_processed = True
# we have to re-read because unlike PIL, not everything reads
# things in string representation :)
queued_file = file(queued_filename, 'rb')
with queued_file:
- main_filepath = create_pub_filepath(entry, queued_filepath[-1])
+ original_filepath = create_pub_filepath(entry, queued_filepath[-1])
- with mgg.public_store.get_file(main_filepath, 'wb') as main_file:
- main_file.write(queued_file.read())
+ with mgg.public_store.get_file(original_filepath, 'wb') as original_file:
+ original_file.write(queued_file.read())
mgg.queue_store.delete_file(queued_filepath)
entry['queued_media_file'] = []
media_files_dict = entry.setdefault('media_files', {})
media_files_dict['thumb'] = thumb_filepath
- media_files_dict['main'] = main_filepath
- media_files_dict['medium'] = medium_filepath
+ media_files_dict['original'] = original_filepath
+ if medium_processed:
+ media_files_dict['medium'] = medium_filepath
entry['state'] = u'processed'
entry.save()
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index 53e019f6..70db6da9 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -1,15 +1,17 @@
body {
- background-color: #272727;
- color: #f7f7f7;
+ background-color: #111;
+ background-image: url("../images/background.png");
+ color: #999;
font-family: sans-serif;
- padding:none;
- margin:0px;
- height:100%;
+ padding: none;
+ margin: 0px;
+ height: 100%;
+ font: 16px "HelveticaNeue-Light","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,sans-serif;
}
form {
- margin:0px;
- padding:0px;
+ margin: 0px;
+ padding: 0px;
}
/* Carter One font */
@@ -24,22 +26,29 @@ form {
/* text styles */
h1{
- font-family: 'Carter One', arial, serif;
+ font-family: 'Carter One',arial,serif;
margin-bottom: 15px;
- margin-top:15px;
+ margin-top: 15px;
+ color: #fff;
+ font-size: 30px;
}
h2{
- margin-top:20px;
+ margin-top: 20px;
+ color: #fff;
}
-p {
- font-family: sans-serif;
- font-size:16px;
+h3{
+ border-bottom: 1px solid #222;
+ font-size: 18px;
}
a {
- color: #86D4B1;
+ color: #999;
+}
+
+a.highlight {
+ color: #fff;
}
label {
@@ -49,192 +58,201 @@ label {
/* website structure */
.mediagoblin_body {
- position:relative;
- min-height:100%;
+ position: relative;
+ min-height: 100%;
}
.mediagoblin_header {
- width:100%;
- height:36px;
- background-color:#393939;
- padding-top:14px;
- margin-bottom:40px;
+ height: 36px;
+ padding-top: 14px;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #222222;
+}
+
+.header_submit{
+ color: #272727;
+ background-color: #aaa;
+ background-image: -webkit-gradient(linear, left top, left bottom, from(##D2D2D2), to(#aaa));
+ background-image: -webkit-linear-gradient(top, #D2D2D2, #aaa);
+ background-image: -moz-linear-gradient(top, #D2D2D2, #aaa);
+ background-image: -ms-linear-gradient(top, #D2D2D2, #aaa);
+ background-image: -o-linear-gradient(top, #D2D2D2, #aaa);
+ background-image: linear-gradient(top, #D2D2D2, #aaa);
+ box-shadow: 0px 0px 4px #000;
+ border-radius: 5px 5px 5px 5px;
+ margin: 8px;
+ padding: 3px 8px;
+ text-decoration: none;
+ border: medium none;
+ font-family: 'Carter One',arial,serif;
}
.mediagoblin_footer {
- width:100%;
- height:26px;
- background-color:#393939;
- bottom:0px;
- padding-top:8px;
- position:absolute;
- text-align:center;
- font-size:14px;
- color:#999;
+ height: 30px;
+ border-top: 1px solid #222222;
+ bottom: 0px;
+ padding-top: 8px;
+ text-align: center;
+ font-size: 14px;
+ color: #999;
}
.mediagoblin_content {
- padding-bottom:74px;
-}
-
-a.mediagoblin_logo {
- width:34px;
- height:25px;
- margin-right:10px;
- background-image:url('../images/icon.png');
- background-position:0px 0px;
- display:inline-block;
-}
-
-a.mediagoblin_logo:hover {
- background-position:0px -28px;
+ padding-bottom: 74px;
}
.mediagoblin_header_right {
- float:right;
+ float: right;
}
/* common website elements */
.button {
- font-family:'Carter One', arial, serif;
- height:32px;
- min-width:99px;
- background-color:#86d4b1;
+ font-family: 'Carter One', arial, serif;
+ height: 32px;
+ min-width: 99px;
+ background-color: #86d4b1;
background-image: -webkit-gradient(linear, left top, left bottom, from(#86d4b1), to(#62caa2));
background-image: -webkit-linear-gradient(top, #86d4b1, #62caa2);
background-image: -moz-linear-gradient(top, #86d4b1, #62caa2);
background-image: -ms-linear-gradient(top, #86d4b1, #62caa2);
background-image: -o-linear-gradient(top, #86d4b1, #62caa2);
background-image: linear-gradient(top, #86d4b1, #62caa2);
- box-shadow:0px 0px 4px #000;
- border-radius:5px;
- border:none;
- color:#272727;
- margin:10px 0px 10px 15px;
- font-size:1em;
- text-align:center;
- padding-left:11px;
- padding-right:11px;
+ box-shadow: 0px 0px 4px #000;
+ border-radius: 5px;
+ border: none;
+ color: #272727;
+ margin: 10px 0px 10px 15px;
+ font-size: 1em;
+ text-align: center;
+ padding-left: 11px;
+ padding-right: 11px;
+ text-decoration: none;
}
.pagination{
-text-align:center;
+text-align: center;
+}
+
+.pagination_arrow{
+ margin: 5px;
}
/* forms */
.form_box {
- background-color:#393939;
- background-image:url("../images/background_lines.png");
- background-repeat:repeat-x;
- font-size:18px;
- padding-bottom:30px;
- padding-top:1px;
- margin-left:auto;
- margin-right:auto;
- display:block;
- float:none;
+ background-color: #222;
+ background-image: url("../images/background_lines.png");
+ background-repeat: repeat-x;
+ font-size: 18px;
+ padding-bottom: 30px;
+ padding-top: 30px;
+ margin-left: auto;
+ margin-right: auto;
+ display: block;
+ float: none;
}
.edit_box {
- background-image:url("../images/background_edit.png");
+ background-image: url("../images/background_edit.png");
}
.form_box h1 {
- font-size:28px;
+ font-size: 28px;
}
.form_field_input input, .form_field_input textarea {
- width:100%;
- font-size:18px;
+ width: 100%;
+ font-size: 18px;
}
.form_field_box {
- margin-bottom:24px;
+ margin-bottom: 24px;
}
.form_field_label,.form_field_input {
- margin-bottom:4px;
+ margin-bottom: 4px;
}
.form_field_error {
- background-color:#87453b;
- border:none;
- font-size:16px;
- padding:9px;
- margin-top:8px;
- margin-bottom:8px;
+ background-color: #87453b;
+ color: #fff;
+ border: none;
+ font-size: 16px;
+ padding: 9px;
+ margin-top: 8px;
+ margin-bottom: 8px;
}
.form_submit_buttons {
- text-align:right;
+ text-align: right;
}
/* comments */
.comment_author {
- margin-bottom:40px;
- padding-top:4px;
+ margin-bottom: 40px;
+ padding-top: 4px;
+ font-size: 14px;
}
.comment_content p {
- margin-bottom:4px;
+ margin-bottom: 4px;
}
/* media galleries */
.media_thumbnail {
- padding:0px;
- width:180px;
- height:180px;
- overflow:hidden;
- float:left;
- margin:0px 4px 10px 4px;
- text-align:center;
+ padding: 0px;
+ width: 180px;
+ height: 180px;
+ overflow: hidden;
+ float: left;
+ margin: 0px 4px 10px 4px;
+ text-align: center;
}
/* icons */
img.media_icon{
- margin:0 4px;
- vertical-align:sub;
+ margin: 0 4px;
+ vertical-align: sub;
}
/* navigation */
.navigation_button{
width: 139px;
- display:block;
- float:left;
+ display: block;
+ float: left;
text-align: center;
- background-color: #393939;
+ background-color: #222;
text-decoration: none;
- padding: 6px 0pt;
+ padding: 12px 0pt;
font-family: 'Carter One', arial, serif;
- font-size:2em;
- margin:0 0 20px
+ font-size: 2em;
+ margin: 0 0 20px
}
p.navigation_button{
- color:#272727;
+ color: #272727;
}
.navigation_left{
- margin-right:2px;
+ margin-right: 2px;
}
/* messages */
ul.mediagoblin_messages {
- list-style:none inside;
- color:#f7f7f7;
+ list-style: none inside;
+ color: #f7f7f7;
}
.mediagoblin_messages li {
- margin:5px 0;
- padding:8px;
- text-align:center;
+ margin: 5px 0;
+ padding: 8px;
+ text-align: center;
}
.message_success {
@@ -255,13 +273,5 @@ ul.mediagoblin_messages {
.message_debug {
background-color: #f7f7f7;
- color:#272727;
-}
-
-/* profile stuff */
-
-.profile_content {
- padding: 6px;
- background-color: #393939;
- margin-bottom: 10px;
+ color: #272727;
}
diff --git a/mediagoblin/static/images/background.png b/mediagoblin/static/images/background.png
new file mode 100644
index 00000000..aa101308
--- /dev/null
+++ b/mediagoblin/static/images/background.png
Binary files differ
diff --git a/mediagoblin/static/images/icon.png b/mediagoblin/static/images/icon.png
deleted file mode 100644
index 4f4f3e9c..00000000
--- a/mediagoblin/static/images/icon.png
+++ /dev/null
Binary files differ
diff --git a/mediagoblin/static/images/logo.png b/mediagoblin/static/images/logo.png
new file mode 100644
index 00000000..cf28a6d4
--- /dev/null
+++ b/mediagoblin/static/images/logo.png
Binary files differ
diff --git a/mediagoblin/static/images/navigation_end.png b/mediagoblin/static/images/navigation_end.png
new file mode 100644
index 00000000..b2f27296
--- /dev/null
+++ b/mediagoblin/static/images/navigation_end.png
Binary files differ
diff --git a/mediagoblin/static/images/navigation_left.png b/mediagoblin/static/images/navigation_left.png
new file mode 100644
index 00000000..d1645120
--- /dev/null
+++ b/mediagoblin/static/images/navigation_left.png
Binary files differ
diff --git a/mediagoblin/static/images/navigation_right.png b/mediagoblin/static/images/navigation_right.png
new file mode 100644
index 00000000..d4caa7b8
--- /dev/null
+++ b/mediagoblin/static/images/navigation_right.png
Binary files differ
diff --git a/mediagoblin/static/images/pagination_left.png b/mediagoblin/static/images/pagination_left.png
new file mode 100644
index 00000000..56a26596
--- /dev/null
+++ b/mediagoblin/static/images/pagination_left.png
Binary files differ
diff --git a/mediagoblin/static/images/pagination_right.png b/mediagoblin/static/images/pagination_right.png
new file mode 100644
index 00000000..84f8abba
--- /dev/null
+++ b/mediagoblin/static/images/pagination_right.png
Binary files differ
diff --git a/mediagoblin/submit/__init__.py b/mediagoblin/submit/__init__.py
index e69de29b..a8eeb5ed 100644
--- a/mediagoblin/submit/__init__.py
+++ b/mediagoblin/submit/__init__.py
@@ -0,0 +1,17 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
diff --git a/mediagoblin/submit/routing.py b/mediagoblin/submit/routing.py
index 3edbab70..5585ecb0 100644
--- a/mediagoblin/submit/routing.py
+++ b/mediagoblin/submit/routing.py
@@ -18,7 +18,4 @@ from routes.route import Route
submit_routes = [
Route('mediagoblin.submit.start', '/',
- controller='mediagoblin.submit.views:submit_start'),
- Route('mediagoblin.submit.success', '/success/',
- template='mediagoblin/submit/success.html',
- controller='mediagoblin.views:simple_template_render')]
+ controller='mediagoblin.submit.views:submit_start')]
diff --git a/mediagoblin/submit/views.py b/mediagoblin/submit/views.py
index c5ac8c62..87e57dda 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -101,8 +101,3 @@ def submit_start(request):
request,
'mediagoblin/submit/start.html',
{'submit_form': submit_form})
-
-
-def submit_success(request):
- return render_to_response(
- request, 'mediagoblin/submit/success.html', {})
diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html
index 2303ce5c..e25783ea 100644
--- a/mediagoblin/templates/mediagoblin/auth/login.html
+++ b/mediagoblin/templates/mediagoblin/auth/login.html
@@ -35,7 +35,9 @@
<input type="hidden" name="next" value="{{ next }}" class="button"
style="display: none;"/>
{% endif %}
- <p>Don't have an account yet?<br /><a href="{{ request.urlgen('mediagoblin.auth.register') }}">Create one here!</a></p>
+ {% if allow_registration %}
+ <p>Don't have an account yet?<br /><a href="{{ request.urlgen('mediagoblin.auth.register') }}">Create one here!</a></p>
+ {% endif %}
</div>
</form>
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/auth/register_success.html b/mediagoblin/templates/mediagoblin/auth/register_success.html
deleted file mode 100644
index cd82a0b9..00000000
--- a/mediagoblin/templates/mediagoblin/auth/register_success.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{#
-# GNU MediaGoblin -- federated, autonomous media hosting
-# Copyright (C) 2011 Free Software Foundation, Inc
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
-{% extends "mediagoblin/base.html" %}
-
-{% block mediagoblin_content %}
- <p>
- Register successful! :D <br />
- You should get a confirmation email soon.
- </p>
-{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/auth/verification_needed.html b/mediagoblin/templates/mediagoblin/auth/verification_needed.html
deleted file mode 100644
index 4104da19..00000000
--- a/mediagoblin/templates/mediagoblin/auth/verification_needed.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{#
-# GNU MediaGoblin -- federated, autonomous media hosting
-# Copyright (C) 2011 Free Software Foundation, Inc
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
-{% extends "mediagoblin/base.html" %}
-
-{% block mediagoblin_content %}
- <p>
- Verfication needed!<br />
- Please check your email to verify your account.
- </p>
-
- <p>
- Still haven't received an email? <a href="{{ request.urlgen('mediagoblin.auth.resend_verification') }}">Click here to resend it.</a>
- </p>
-{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html
index b71fca24..656fb46b 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -14,9 +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/>.
-#}
+-#}
+<!doctype html>
<html>
<head>
+ <meta charset="utf-8">
<title>{% block title %}GNU MediaGoblin{% endblock title %}</title>
<link rel="stylesheet" type="text/css"
href="{{ request.staticdirect('/css/contrib/reset.css') }}"/>
@@ -34,23 +36,41 @@
{% block mediagoblin_body %}
<div class="mediagoblin_body">
{% block mediagoblin_header %}
- <div class="mediagoblin_header">
- <div class="container_16">
- <div class="grid_16">
- {% block mediagoblin_logo %}
- <a class="mediagoblin_logo" href="{{ request.urlgen('index') }}"></a>
- {% endblock %}{% block mediagoblin_header_title %}{% endblock %}
- <div class="mediagoblin_header_right">
- {% if request.user %}
+ <div class="container_16">
+ <div class="grid_16 mediagoblin_header">
+ {% block mediagoblin_logo %}
+ <a class="mediagoblin_logo"
+ href="{{ request.urlgen('index') }}">
+ <img src="{{ request.staticdirect('/images/logo.png') }}"
+ alt="Mediagoblin logo" />
+ </a>
+ {% endblock %}
+ {% if request.user and request.user['status'] == 'active' %}
+ <a class="header_submit"
+ href="{{ request.urlgen('mediagoblin.submit.start') }}">
+ Submit media
+ </a>
+ {% endif %}
+ {% block mediagoblin_header_title %}{% endblock %}
+ <div class="mediagoblin_header_right">
+ {% if request.user %}
+ {# the following link should only appear when verification is needed #}
+ {% if request.user.status == "needs_email_verification" %}
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
- user= request.user['username']) }}">
- {{ request.user['username'] }}</a>'s account
- (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">logout</a>)
- {% else %}
- <a href="{{ request.urlgen('mediagoblin.auth.login') }}">
- Login</a>
+ user=request.user['username']) }}"
+ class="header_submit">
+ needs verification!</a>
{% endif %}
- </div>
+
+ <a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
+ user= request.user['username']) }}">
+ {{ request.user['username'] }}</a>
+
+ (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">logout</a>)
+ {% else %}
+ <a href="{{ request.urlgen('mediagoblin.auth.login') }}">
+ Login</a>
+ {% endif %}
</div>
</div>
</div>
@@ -63,11 +83,9 @@
</div>
</div>
{% block mediagoblin_footer %}
- <div class="mediagoblin_footer">
- <div class="container_16">
- <div class="grid_16">
- Powered by <a href="http://mediagoblin.org">MediaGoblin</a>, a <a href="http://gnu.org/">GNU project</a>
- </div>
+ <div class="container_16">
+ <div class="grid_16 mediagoblin_footer">
+ Powered by <a href="http://mediagoblin.org">MediaGoblin</a>, a <a href="http://gnu.org/">GNU project</a>
</div>
</div>
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/root.html b/mediagoblin/templates/mediagoblin/root.html
index 5b744999..bae033c4 100644
--- a/mediagoblin/templates/mediagoblin/root.html
+++ b/mediagoblin/templates/mediagoblin/root.html
@@ -29,10 +29,12 @@
If you have an account, you can
<a href="{{ request.urlgen('mediagoblin.auth.login') }}">Login</a>.
</p>
- <p>
- If you don't have an account, please
- <a href="{{ request.urlgen('mediagoblin.auth.register') }}">Register</a>.
- </p>
+ {% if allow_registration %}
+ <p>
+ If you don't have an account, please
+ <a href="{{ request.urlgen('mediagoblin.auth.register') }}">Register</a>.
+ </p>
+ {% endif %}
{% endif %}
{# temporarily, an "image gallery" that isn't one really ;) #}
diff --git a/mediagoblin/templates/mediagoblin/submit/success.html b/mediagoblin/templates/mediagoblin/submit/success.html
deleted file mode 100644
index afc9f9d1..00000000
--- a/mediagoblin/templates/mediagoblin/submit/success.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{#
-# GNU MediaGoblin -- federated, autonomous media hosting
-# Copyright (C) 2011 Free Software Foundation, Inc
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#}
-{% extends "mediagoblin/base.html" %}
-
-{% block mediagoblin_content %}
- Woohoo! Submitted!
-{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index 8dd42115..7622d6e6 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -23,13 +23,8 @@
{% block mediagoblin_content %}
{% if media %}
<div class="grid_11 alpha">
- {% if media.media_files.medium %}
- <img src="{{ request.app.public_store.file_url(
- media.media_files.medium) }}" />
- {% else %}
- <img src="{{ request.app.public_store.file_url(
- media.media_files.main) }}" />
- {% endif %}
+ <img class="media_image" src="{{ request.app.public_store.file_url(
+ media.get_display_media(media.media_files)) }}" />
<h2>
{{media.title}}
@@ -55,7 +50,7 @@
<form action="{{ request.urlgen('mediagoblin.user_pages.media_post_comment',
user= media.uploader().username,
media=media._id) }}" method="POST">
- {{ wtforms_util.render_field_div(comment_form.comment) }}
+ {{ wtforms_util.render_field_div(comment_form.comment_content) }}
<div class="form_submit_buttons">
<input type="submit" value="Post comment!" class="button" />
</div>
@@ -65,7 +60,12 @@
{% if comments %}
{% for comment in comments %}
{% set comment_author = comment.author() %}
- <div class="comment_wrapper" id="comment-{{ comment['_id'] }}">
+ {% 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_content">
{% autoescape False %}
{{ comment.content_html }}
@@ -77,7 +77,10 @@
{{ comment_author['username'] }}</a> at
<!--</div>
<div class="comment_datetime">-->
- <a href="#comment-{{ comment['_id'] }}">
+ <a href="{{ request.urlgen('mediagoblin.user_pages.media_home.view_comment',
+ comment = comment['_id'],
+ user = media.uploader().username,
+ media = media._id) }}#comment">
{{ "%4d-%02d-%02d %02d:%02d"|format(comment.created.year,
comment.created.month,
comment.created.day,
@@ -88,7 +91,10 @@
</div>
{% endfor %}
- {{ render_pagination(request, pagination) }}
+ {{ render_pagination(request, pagination,
+ request.urlgen('mediagoblin.user_pages.media_home',
+ user = media.uploader().username,
+ media = media._id)) }}
</div>
{% endif %}
<div class="grid_5 omega">
diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html
index aed330c8..7769b8b3 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/user.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/user.html
@@ -25,34 +25,67 @@
{% endblock mediagoblin_head %}
{% block mediagoblin_content -%}
- {% if user %}
- <h1>{{ user.username }}'s profile</h1>
+ {# If no user... #}
+ {% if not user %}
+ <p>Sorry, no such user found.<p/>
- {% include "mediagoblin/utils/profile.html" %}
+ {# User exists, but needs verification #}
+ {% elif user.status == "needs_email_verification" %}
+ {% if user == request.user %}
+ {# this should only be visible when you are this user #}
+ <div class="grid_6 prefix_1 suffix_1 form_box">
+ <h1>Verification needed</h1>
- {% if request.user['_id'] == user['_id'] or request.user['is_admin'] %}
- <a href="{{ request.urlgen('mediagoblin.edit.profile') }}?username={{
- user.username }}">Edit profile</a>
- {% endif %}
+ <p>Almost done! Your account still needs to be verified.</p>
+ <p>
+ An email should arrive in a few moments with instructions
+ on how to do so.
+ </p>
+ <p>In case it doesn't:</p>
+
+ <a href="{{ request.urlgen('mediagoblin.auth.resend_verification') }}"
+ class="button">Resend verification email</a>
+ </div>
+ {% else %}
+ {# if the user is not you, but still needs to verify their email #}
+ <div class="grid_6 prefix_1 suffix_1 form_box">
+ <h1>Verification needed</h1>
- {% if request.user['_id'] == user['_id'] %}
- <p>
- <a href="{{ request.urlgen('mediagoblin.submit.start') }}">Submit an item</a>
- </p>
+ <p>
+ Someone has registered an account with this username, but it
+ still has to be verified.
+ </p>
+
+ <p>
+ If you are that person but you've lost your verification
+ email, you can
+ <a href="{{ request.urlgen('mediagoblin.auth.login') }}">log in</a>
+ and resend it.
+ </p>
+ </div>
{% endif %}
- {% set pagination_base_url = user_gallery_url %}
- {% include "mediagoblin/utils/object_gallery.html" %}
+ {# Active(?) (or at least verified at some point) user, horray! #}
+ {% else %}
+ <h1>{{ user.username }}'s profile</h1>
- <div class="clear"></div>
+ <div class="grid_6 alpha">
+ {% include "mediagoblin/utils/profile.html" %}
+ {% if request.user['_id'] == user['_id'] or request.user['is_admin'] %}
+ <a href="{{ request.urlgen('mediagoblin.edit.profile') }}?username={{
+ user.username }}">Edit profile</a>
+ {% endif %}
+ </div>
- <p><a href="{{ user_gallery_url }}">View all of {{ user.username }}'s media</a></p>
+ <div class="grid_10 omega">
+ {% set pagination_base_url = user_gallery_url %}
+ {% include "mediagoblin/utils/object_gallery.html" %}
+ <p><a href="{{ user_gallery_url }}">View all of {{ user.username }}'s media</a></p>
+ <a href={{ request.urlgen(
+ 'mediagoblin.user_pages.atom_feed',
+ user=user.username) }}>atom feed</a>
+ </div>
- <a href={{ request.urlgen(
- 'mediagoblin.user_pages.atom_feed',
- user=user.username) }}>atom feed</a>
- {% else %}
- {# This *should* not occur as the view makes sure we pass in a user. #}
- <p>Sorry, no such user found.<p/>
+ <div class="clear"></div>
{% endif %}
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/utils/pagination.html b/mediagoblin/templates/mediagoblin/utils/pagination.html
index aae50d22..23d49463 100644
--- a/mediagoblin/templates/mediagoblin/utils/pagination.html
+++ b/mediagoblin/templates/mediagoblin/utils/pagination.html
@@ -34,9 +34,16 @@
{% if pagination.has_prev %}
<a href="{{ pagination.get_page_url_explicit(
base_url, get_params,
- pagination.page - 1) }}">&laquo; Prev</a>
+ pagination.page - 1) }}"><img class="pagination_arrow" src="/mgoblin_static/images/pagination_left.png" alt="Previous page" />Newer</a>
{% endif %}
-
+ {% if pagination.has_next %}
+ <a href="{{ pagination.get_page_url_explicit(
+ base_url, get_params,
+ pagination.page + 1) }}">Older<img class="pagination_arrow" src="/mgoblin_static/images/pagination_right.png" alt="Next page" />
+ </a>
+ {% endif %}
+ <br />
+ Go to page:
{%- for page in pagination.iter_pages() %}
{% if page %}
{% if page != pagination.page %}
@@ -50,12 +57,6 @@
<span class="ellipsis">…</span>
{% endif %}
{%- endfor %}
-
- {% if pagination.has_next %}
- <a href="{{ pagination.get_page_url_explicit(
- base_url, get_params,
- pagination.page + 1) }}">Next &raquo;</a>
- {% endif %}
</p>
</div>
{% endif %}
diff --git a/mediagoblin/templates/mediagoblin/utils/prev_next.html b/mediagoblin/templates/mediagoblin/utils/prev_next.html
index 8908c298..7cf8d2a4 100644
--- a/mediagoblin/templates/mediagoblin/utils/prev_next.html
+++ b/mediagoblin/templates/mediagoblin/utils/prev_next.html
@@ -24,20 +24,23 @@
{# There are no previous entries for the very first media entry #}
{% if prev_entry_url %}
<a class="navigation_button navigation_left" href="{{ prev_entry_url }}">
- &lt;
+ <img src="/mgoblin_static/images/navigation_left.png" alt="Previous image" />
</a>
{% else %}
{# This is the first entry. display greyed-out 'previous' image #}
- <p class="navigation_button">X</p>
+ <p class="navigation_button navigation_left">
+ <img src="/mgoblin_static/images/navigation_end.png" alt="No previous images" />
+ </p>
{% endif %}
-
{# Likewise, this could be the very last media entry #}
{% if next_entry_url %}
<a class="navigation_button" href="{{ next_entry_url }}">
- &gt;
+ <img src="/mgoblin_static/images/navigation_right.png" alt="Next image" />
</a>
{% else %}
{# This is the last entry. display greyed-out 'next' image #}
- <p class="navigation_button">X</p>
+ <p class="navigation_button">
+ <img src="/mgoblin_static/images/navigation_end.png" alt="No following images" />
+ </p>
{% endif %}
</div>
diff --git a/mediagoblin/templates/mediagoblin/utils/profile.html b/mediagoblin/templates/mediagoblin/utils/profile.html
index cd60bbfc..63024b77 100644
--- a/mediagoblin/templates/mediagoblin/utils/profile.html
+++ b/mediagoblin/templates/mediagoblin/utils/profile.html
@@ -17,19 +17,14 @@
#}
{% block profile_content -%}
- {% if user.url or user.bio %}
- <div class="profile_content">
- {% if user.url %}
- <div class="profile_homepage">
- <a href="{{ user.url }}">{{ user.url }}</a>
- </div>
- {% endif %}
-
- {% if user.bio %}
- <div class="profile_bio">
- {{ user.bio }}
- </div>
- {% endif %}
- </div>
+ {% if user.bio %}
+ {% autoescape False %}
+ <p>{{ user.bio_html }}</p>
+ {% endautoescape %}
{% endif %}
-{% endblock %}
+ {% if user.url %}
+ <p>
+ <a href="{{ user.url }}">{{ user.url }}</a>
+ </p>
+ {% endif %}
+{% endblock %}
diff --git a/mediagoblin/tests/test_auth.py b/mediagoblin/tests/test_auth.py
index ad9dd35b..f0bb183f 100644
--- a/mediagoblin/tests/test_auth.py
+++ b/mediagoblin/tests/test_auth.py
@@ -153,9 +153,9 @@ def test_register_views(test_app):
## Did we redirect to the proper page? Use the right template?
assert_equal(
urlparse.urlsplit(response.location)[2],
- '/auth/register/success/')
+ '/u/happygirl/')
assert util.TEMPLATE_TEST_CONTEXT.has_key(
- 'mediagoblin/auth/register_success.html')
+ 'mediagoblin/user_pages/user.html')
## Make sure user is in place
new_user = mg_globals.database.User.find_one(
@@ -164,6 +164,11 @@ def test_register_views(test_app):
assert new_user['status'] == u'needs_email_verification'
assert new_user['email_verified'] == False
+ ## Make sure user is logged in
+ request = util.TEMPLATE_TEST_CONTEXT[
+ 'mediagoblin/user_pages/user.html']['request']
+ assert request.session['user_id'] == unicode(new_user['_id'])
+
## Make sure we get email confirmation, and try verifying
assert len(util.EMAIL_TEST_INBOX) == 1
message = util.EMAIL_TEST_INBOX.pop()
diff --git a/mediagoblin/tests/test_migrations.py b/mediagoblin/tests/test_migrations.py
new file mode 100644
index 00000000..c100a26a
--- /dev/null
+++ b/mediagoblin/tests/test_migrations.py
@@ -0,0 +1,403 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from nose.tools import assert_raises
+from pymongo import Connection
+
+from mediagoblin.tests.tools import (
+ install_fixtures_simple, assert_db_meets_expected)
+from mediagoblin.db.util import (
+ RegisterMigration, MigrationManager, ObjectId,
+ MissingCurrentMigration)
+
+# This one will get filled with local migrations
+TEST_MIGRATION_REGISTRY = {}
+# this one won't get filled
+TEST_EMPTY_MIGRATION_REGISTRY = {}
+
+MIGRATION_DB_NAME = u'__mediagoblin_test_migrations__'
+
+
+######################
+# Fake test migrations
+######################
+
+@RegisterMigration(1, TEST_MIGRATION_REGISTRY)
+def creature_add_magical_powers(database):
+ """
+ Add lists of magical powers.
+
+ This defaults to [], an empty list. Since we haven't declared any
+ magical powers, all existing monsters, setting to an empty list is
+ fine.
+ """
+ database['creatures'].update(
+ {'magical_powers': {'$exists': False}},
+ {'$set': {'magical_powers': []}},
+ multi=True)
+
+
+@RegisterMigration(2, TEST_MIGRATION_REGISTRY)
+def creature_rename_num_legs_to_num_limbs(database):
+ """
+ It turns out we want to track how many limbs a creature has, not
+ just how many legs. We don't care about the ambiguous distinction
+ between arms/legs currently.
+ """
+ # $rename not available till 1.7.2+, Debian Stable only includes
+ # 1.4.4... we should do renames manually for now :(
+
+ collection = database['creatures']
+ target = collection.find(
+ {'num_legs': {'$exists': True}})
+
+ for document in target:
+ # A lame manual renaming.
+ document['num_limbs'] = document.pop('num_legs')
+ collection.save(document)
+
+
+@RegisterMigration(3, TEST_MIGRATION_REGISTRY)
+def creature_remove_is_demon(database):
+ """
+ It turns out we don't care much about whether creatures are demons
+ or not.
+ """
+ database['creatures'].update(
+ {'is_demon': {'$exists': True}},
+ {'$unset': {'is_demon': 1}},
+ multi=True)
+
+
+@RegisterMigration(4, TEST_MIGRATION_REGISTRY)
+def level_exits_dict_to_list(database):
+ """
+ For the sake of the indexes we want to write, and because we
+ intend to add more flexible fields, we want to move level exits
+ from like:
+
+ {'big_door': 'castle_level_id',
+ 'trapdoor': 'dungeon_level_id'}
+
+ to like:
+
+ [{'name': 'big_door',
+ 'exits_to': 'castle_level_id'},
+ {'name': 'trapdoor',
+ 'exits_to': 'dungeon_level_id'}]
+ """
+ collection = database['levels']
+ target = collection.find(
+ {'exits': {'$type': 3}})
+
+ for level in target:
+ new_exits = []
+ for exit_name, exits_to in level['exits'].items():
+ new_exits.append(
+ {'name': exit_name,
+ 'exits_to': exits_to})
+
+ level['exits'] = new_exits
+ collection.save(level)
+
+
+CENTIPEDE_OBJECTID = ObjectId()
+WOLF_OBJECTID = ObjectId()
+WIZARDSNAKE_OBJECTID = ObjectId()
+
+UNMIGRATED_DBDATA = {
+ 'creatures': [
+ {'_id': CENTIPEDE_OBJECTID,
+ 'name': 'centipede',
+ 'num_legs': 100,
+ 'is_demon': False},
+ {'_id': WOLF_OBJECTID,
+ 'name': 'wolf',
+ 'num_legs': 4,
+ 'is_demon': False},
+ # don't ask me what a wizardsnake is.
+ {'_id': WIZARDSNAKE_OBJECTID,
+ 'name': 'wizardsnake',
+ 'num_legs': 0,
+ 'is_demon': True}],
+ 'levels': [
+ {'_id': 'necroplex',
+ 'name': 'The Necroplex',
+ 'description': 'A complex full of pure deathzone.',
+ 'exits': {
+ 'deathwell': 'evilstorm',
+ 'portal': 'central_park'}},
+ {'_id': 'evilstorm',
+ 'name': 'Evil Storm',
+ 'description': 'A storm full of pure evil.',
+ 'exits': {}}, # you can't escape the evilstorm
+ {'_id': 'central_park',
+ 'name': 'Central Park, NY, NY',
+ 'description': "New York's friendly Central Park.",
+ 'exits': {
+ 'portal': 'necroplex'}}]}
+
+
+EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA = {
+ 'creatures': [
+ {'_id': CENTIPEDE_OBJECTID,
+ 'name': 'centipede',
+ 'num_limbs': 100,
+ 'magical_powers': []},
+ {'_id': WOLF_OBJECTID,
+ 'name': 'wolf',
+ 'num_limbs': 4,
+ # kept around namely to check that it *isn't* removed!
+ 'magical_powers': []},
+ {'_id': WIZARDSNAKE_OBJECTID,
+ 'name': 'wizardsnake',
+ 'num_limbs': 0,
+ 'magical_powers': []}],
+ 'levels': [
+ {'_id': 'necroplex',
+ 'name': 'The Necroplex',
+ 'description': 'A complex full of pure deathzone.',
+ 'exits': [
+ {'name': 'deathwell',
+ 'exits_to': 'evilstorm'},
+ {'name': 'portal',
+ 'exits_to': 'central_park'}]},
+ {'_id': 'evilstorm',
+ 'name': 'Evil Storm',
+ 'description': 'A storm full of pure evil.',
+ 'exits': []}, # you can't escape the evilstorm
+ {'_id': 'central_park',
+ 'name': 'Central Park, NY, NY',
+ 'description': "New York's friendly Central Park.",
+ 'exits': [
+ {'name': 'portal',
+ 'exits_to': 'necroplex'}]}]}
+
+# We want to make sure that if we're at migration 3, migration 3
+# doesn't get re-run.
+
+SEMI_MIGRATED_DBDATA = {
+ 'creatures': [
+ {'_id': CENTIPEDE_OBJECTID,
+ 'name': 'centipede',
+ 'num_limbs': 100,
+ 'magical_powers': []},
+ {'_id': WOLF_OBJECTID,
+ 'name': 'wolf',
+ 'num_limbs': 4,
+ # kept around namely to check that it *isn't* removed!
+ 'is_demon': False,
+ 'magical_powers': [
+ 'ice_breath', 'death_stare']},
+ {'_id': WIZARDSNAKE_OBJECTID,
+ 'name': 'wizardsnake',
+ 'num_limbs': 0,
+ 'magical_powers': [
+ 'death_rattle', 'sneaky_stare',
+ 'slithery_smoke', 'treacherous_tremors'],
+ 'is_demon': True}],
+ 'levels': [
+ {'_id': 'necroplex',
+ 'name': 'The Necroplex',
+ 'description': 'A complex full of pure deathzone.',
+ 'exits': {
+ 'deathwell': 'evilstorm',
+ 'portal': 'central_park'}},
+ {'_id': 'evilstorm',
+ 'name': 'Evil Storm',
+ 'description': 'A storm full of pure evil.',
+ 'exits': {}}, # you can't escape the evilstorm
+ {'_id': 'central_park',
+ 'name': 'Central Park, NY, NY',
+ 'description': "New York's friendly Central Park.",
+ 'exits': {
+ 'portal': 'necroplex'}}]}
+
+
+EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA = {
+ 'creatures': [
+ {'_id': CENTIPEDE_OBJECTID,
+ 'name': 'centipede',
+ 'num_limbs': 100,
+ 'magical_powers': []},
+ {'_id': WOLF_OBJECTID,
+ 'name': 'wolf',
+ 'num_limbs': 4,
+ # kept around namely to check that it *isn't* removed!
+ 'is_demon': False,
+ 'magical_powers': [
+ 'ice_breath', 'death_stare']},
+ {'_id': WIZARDSNAKE_OBJECTID,
+ 'name': 'wizardsnake',
+ 'num_limbs': 0,
+ 'magical_powers': [
+ 'death_rattle', 'sneaky_stare',
+ 'slithery_smoke', 'treacherous_tremors'],
+ 'is_demon': True}],
+ 'levels': [
+ {'_id': 'necroplex',
+ 'name': 'The Necroplex',
+ 'description': 'A complex full of pure deathzone.',
+ 'exits': [
+ {'name': 'deathwell',
+ 'exits_to': 'evilstorm'},
+ {'name': 'portal',
+ 'exits_to': 'central_park'}]},
+ {'_id': 'evilstorm',
+ 'name': 'Evil Storm',
+ 'description': 'A storm full of pure evil.',
+ 'exits': []}, # you can't escape the evilstorm
+ {'_id': 'central_park',
+ 'name': 'Central Park, NY, NY',
+ 'description': "New York's friendly Central Park.",
+ 'exits': [
+ {'name': 'portal',
+ 'exits_to': 'necroplex'}]}]}
+
+
+class TestMigrations(object):
+ def setUp(self):
+ # Set up the connection, drop an existing possible database
+ self.connection = Connection()
+ self.connection.drop_database(MIGRATION_DB_NAME)
+ self.db = Connection()[MIGRATION_DB_NAME]
+ self.migration_manager = MigrationManager(
+ self.db, TEST_MIGRATION_REGISTRY)
+ self.empty_migration_manager = MigrationManager(
+ self.db, TEST_EMPTY_MIGRATION_REGISTRY)
+ self.run_migrations = []
+
+ def tearDown(self):
+ self.connection.drop_database(MIGRATION_DB_NAME)
+
+ def _record_migration(self, migration_number, migration_func):
+ self.run_migrations.append((migration_number, migration_func))
+
+ def test_migrations_registered_and_sorted(self):
+ """
+ Make sure that migrations get registered and are sorted right
+ in the migration manager
+ """
+ assert TEST_MIGRATION_REGISTRY == {
+ 1: creature_add_magical_powers,
+ 2: creature_rename_num_legs_to_num_limbs,
+ 3: creature_remove_is_demon,
+ 4: level_exits_dict_to_list}
+ assert self.migration_manager.sorted_migrations == [
+ (1, creature_add_magical_powers),
+ (2, creature_rename_num_legs_to_num_limbs),
+ (3, creature_remove_is_demon),
+ (4, level_exits_dict_to_list)]
+ assert self.empty_migration_manager.sorted_migrations == []
+
+ def test_run_full_migrations(self):
+ """
+ Make sure that running the full migration suite from 0 updates
+ everything
+ """
+ self.migration_manager.set_current_migration(0)
+ assert self.migration_manager.database_current_migration() == 0
+ install_fixtures_simple(self.db, UNMIGRATED_DBDATA)
+ self.migration_manager.migrate_new(post_callback=self._record_migration)
+
+ assert self.run_migrations == [
+ (1, creature_add_magical_powers),
+ (2, creature_rename_num_legs_to_num_limbs),
+ (3, creature_remove_is_demon),
+ (4, level_exits_dict_to_list)]
+
+ assert_db_meets_expected(
+ self.db, EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA)
+
+ # Make sure the migration is recorded correctly
+ assert self.migration_manager.database_current_migration() == 4
+
+ # run twice! It should do nothing the second time.
+ # ------------------------------------------------
+ self.run_migrations = []
+ self.migration_manager.migrate_new(post_callback=self._record_migration)
+ assert self.run_migrations == []
+ assert_db_meets_expected(
+ self.db, EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA)
+ assert self.migration_manager.database_current_migration() == 4
+
+
+ def test_run_partial_migrations(self):
+ """
+ Make sure that running full migration suite from 3 only runs
+ last migration
+ """
+ self.migration_manager.set_current_migration(3)
+ assert self.migration_manager.database_current_migration() == 3
+ install_fixtures_simple(self.db, SEMI_MIGRATED_DBDATA)
+ self.migration_manager.migrate_new(post_callback=self._record_migration)
+
+ assert self.run_migrations == [
+ (4, level_exits_dict_to_list)]
+
+ assert_db_meets_expected(
+ self.db, EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA)
+
+ # Make sure the migration is recorded correctly
+ assert self.migration_manager.database_current_migration() == 4
+
+ def test_migrations_recorded_as_latest(self):
+ """
+ Make sure that if we don't have a migration_status
+ pre-recorded it's marked as the latest
+ """
+ self.migration_manager.install_migration_version_if_missing()
+ assert self.migration_manager.database_current_migration() == 4
+
+ def test_no_migrations_recorded_as_zero(self):
+ """
+ Make sure that if we don't have a migration_status
+ but there *are* no migrations that it's marked as 0
+ """
+ self.empty_migration_manager.install_migration_version_if_missing()
+ assert self.empty_migration_manager.database_current_migration() == 0
+
+ def test_migrations_to_run(self):
+ """
+ Make sure we get the right list of migrations to run
+ """
+ self.migration_manager.set_current_migration(0)
+
+ assert self.migration_manager.migrations_to_run() == [
+ (1, creature_add_magical_powers),
+ (2, creature_rename_num_legs_to_num_limbs),
+ (3, creature_remove_is_demon),
+ (4, level_exits_dict_to_list)]
+
+ self.migration_manager.set_current_migration(3)
+
+ assert self.migration_manager.migrations_to_run() == [
+ (4, level_exits_dict_to_list)]
+
+ self.migration_manager.set_current_migration(4)
+
+ assert self.migration_manager.migrations_to_run() == []
+
+
+ def test_no_migrations_raises_exception(self):
+ """
+ If we don't have the current migration set in the database,
+ this should error out.
+ """
+ assert_raises(
+ MissingCurrentMigration,
+ self.migration_manager.migrations_to_run)
diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py
new file mode 100644
index 00000000..22b6117c
--- /dev/null
+++ b/mediagoblin/tests/test_submission.py
@@ -0,0 +1,157 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import urlparse
+import pkg_resources
+
+from nose.tools import assert_equal
+
+from mediagoblin.auth import lib as auth_lib
+from mediagoblin.tests.tools import setup_fresh_app, get_test_app
+from mediagoblin import mg_globals
+from mediagoblin import util
+
+GOOD_JPG = pkg_resources.resource_filename(
+ 'mediagoblin.tests', 'test_submission/good.jpg')
+GOOD_PNG = pkg_resources.resource_filename(
+ 'mediagoblin.tests', 'test_submission/good.png')
+EVIL_FILE = pkg_resources.resource_filename(
+ 'mediagoblin.tests', 'test_submission/evil')
+EVIL_JPG = pkg_resources.resource_filename(
+ 'mediagoblin.tests', 'test_submission/evil.jpg')
+EVIL_PNG = pkg_resources.resource_filename(
+ 'mediagoblin.tests', 'test_submission/evil.png')
+
+
+class TestSubmission:
+ def setUp(self):
+ self.test_app = get_test_app()
+
+ # TODO: Possibly abstract into a decorator like:
+ # @as_authenticated_user('chris')
+ test_user = mg_globals.database.User()
+ test_user['username'] = u'chris'
+ test_user['email'] = u'chris@example.com'
+ test_user['email_verified'] = True
+ test_user['status'] = u'active'
+ test_user['pw_hash'] = auth_lib.bcrypt_gen_password_hash('toast')
+ test_user.save()
+
+ self.test_app.post(
+ '/auth/login/', {
+ 'username': u'chris',
+ 'password': 'toast'})
+
+ def test_missing_fields(self):
+ # Test blank form
+ # ---------------
+ util.clear_test_template_context()
+ response = self.test_app.post(
+ '/submit/', {})
+ context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html']
+ form = context['submit_form']
+ assert form.file.errors == [u'You must provide a file.']
+
+ # Test blank file
+ # ---------------
+ util.clear_test_template_context()
+ response = self.test_app.post(
+ '/submit/', {
+ 'title': 'test title'})
+ context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html']
+ form = context['submit_form']
+ assert form.file.errors == [u'You must provide a file.']
+
+
+ def test_normal_uploads(self):
+ # Test JPG
+ # --------
+ util.clear_test_template_context()
+ response = self.test_app.post(
+ '/submit/', {
+ 'title': 'Normal upload 1'
+ }, upload_files=[(
+ 'file', GOOD_JPG)])
+
+ # User should be redirected
+ response.follow()
+ assert_equal(
+ urlparse.urlsplit(response.location)[2],
+ '/u/chris/')
+ assert util.TEMPLATE_TEST_CONTEXT.has_key(
+ 'mediagoblin/user_pages/user.html')
+
+ # Test PNG
+ # --------
+ util.clear_test_template_context()
+ response = self.test_app.post(
+ '/submit/', {
+ 'title': 'Normal upload 2'
+ }, upload_files=[(
+ 'file', GOOD_PNG)])
+
+ response.follow()
+ assert_equal(
+ urlparse.urlsplit(response.location)[2],
+ '/u/chris/')
+ assert util.TEMPLATE_TEST_CONTEXT.has_key(
+ 'mediagoblin/user_pages/user.html')
+
+
+ def test_malicious_uploads(self):
+ # Test non-suppoerted file with non-supported extension
+ # -----------------------------------------------------
+ util.clear_test_template_context()
+ response = self.test_app.post(
+ '/submit/', {
+ 'title': 'Malicious Upload 2'
+ }, upload_files=[(
+ 'file', EVIL_FILE)])
+
+ context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html']
+ form = context['submit_form']
+ assert form.file.errors == ['The file doesn\'t seem to be an image!']
+
+ # NOTE: The following 2 tests will fail. These can be uncommented
+ # after http://bugs.foocorp.net/issues/324 is resolved and
+ # bad files are handled properly.
+
+ # Test non-supported file with .jpg extension
+ # -------------------------------------------
+ #util.clear_test_template_context()
+ #response = self.test_app.post(
+ # '/submit/', {
+ # 'title': 'Malicious Upload 2'
+ # }, upload_files=[(
+ # 'file', EVIL_JPG)])
+
+ #context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html']
+ #form = context['submit_form']
+ #assert form.file.errors == ['The file doesn\'t seem to be an image!']
+
+ # Test non-supported file with .png extension
+ # -------------------------------------------
+ #util.clear_test_template_context()
+ #response = self.test_app.post(
+ # '/submit/', {
+ # 'title': 'Malicious Upload 3'
+ # }, upload_files=[(
+ # 'file', EVIL_PNG)])
+
+ #context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/submit/start.html']
+ #form = context['submit_form']
+ #assert form.file.errors == ['The file doesn\'t seem to be an image!']
+
diff --git a/mediagoblin/tests/test_submission/evil b/mediagoblin/tests/test_submission/evil
new file mode 100755
index 00000000..775da664
--- /dev/null
+++ b/mediagoblin/tests/test_submission/evil
Binary files differ
diff --git a/mediagoblin/tests/test_submission/evil.jpg b/mediagoblin/tests/test_submission/evil.jpg
new file mode 100755
index 00000000..775da664
--- /dev/null
+++ b/mediagoblin/tests/test_submission/evil.jpg
Binary files differ
diff --git a/mediagoblin/tests/test_submission/evil.png b/mediagoblin/tests/test_submission/evil.png
new file mode 100755
index 00000000..775da664
--- /dev/null
+++ b/mediagoblin/tests/test_submission/evil.png
Binary files differ
diff --git a/mediagoblin/tests/test_submission/good.jpg b/mediagoblin/tests/test_submission/good.jpg
new file mode 100644
index 00000000..936458e9
--- /dev/null
+++ b/mediagoblin/tests/test_submission/good.jpg
Binary files differ
diff --git a/mediagoblin/tests/test_submission/good.png b/mediagoblin/tests/test_submission/good.png
new file mode 100644
index 00000000..c1eadf9c
--- /dev/null
+++ b/mediagoblin/tests/test_submission/good.png
Binary files differ
diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py
index e56af4de..4b61f259 100644
--- a/mediagoblin/tests/tools.py
+++ b/mediagoblin/tests/tools.py
@@ -118,3 +118,35 @@ def setup_fresh_app(func):
return func(test_app, *args, **kwargs)
return _make_safe(wrapper, func)
+
+
+def install_fixtures_simple(db, fixtures):
+ """
+ Very simply install fixtures in the database
+ """
+ for collection_name, collection_fixtures in fixtures.iteritems():
+ collection = db[collection_name]
+ for fixture in collection_fixtures:
+ collection.insert(fixture)
+
+
+def assert_db_meets_expected(db, expected):
+ """
+ Assert a database contains the things we expect it to.
+
+ Objects are found via '_id', so you should make sure your document
+ has an _id.
+
+ Args:
+ - db: pymongo or mongokit database connection
+ - expected: the data we expect. Formatted like:
+ {'collection_name': [
+ {'_id': 'foo',
+ 'some_field': 'some_value'},]}
+ """
+ for collection_name, collection_data in expected.iteritems():
+ collection = db[collection_name]
+ for expected_document in collection_data:
+ document = collection.find_one({'_id': expected_document['_id']})
+ assert document is not None # make sure it exists
+ assert document == expected_document # make sure it matches
diff --git a/mediagoblin/user_pages/__init__.py b/mediagoblin/user_pages/__init__.py
index e69de29b..a8eeb5ed 100644
--- a/mediagoblin/user_pages/__init__.py
+++ b/mediagoblin/user_pages/__init__.py
@@ -0,0 +1,17 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
diff --git a/mediagoblin/user_pages/forms.py b/mediagoblin/user_pages/forms.py
index 9f7d2fbd..8829b674 100644
--- a/mediagoblin/user_pages/forms.py
+++ b/mediagoblin/user_pages/forms.py
@@ -1,21 +1,22 @@
-# GNU MediaGoblin -- federated, autonomous media hosting
-# Copyright (C) 2011 Free Software Foundation, Inc
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import wtforms
-
-class MediaCommentForm(wtforms.Form):
- comment = wtforms.TextAreaField('Comment',
- [wtforms.validators.Required()]) \ No newline at end of file
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011 Free Software Foundation, Inc
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import wtforms
+
+class MediaCommentForm(wtforms.Form):
+ comment_content = wtforms.TextAreaField(
+ 'Comment',
+ [wtforms.validators.Required()])
diff --git a/mediagoblin/user_pages/routing.py b/mediagoblin/user_pages/routing.py
index 255b6f66..3be0617d 100644
--- a/mediagoblin/user_pages/routing.py
+++ b/mediagoblin/user_pages/routing.py
@@ -24,6 +24,9 @@ user_routes = [
Route('mediagoblin.user_pages.media_home', '/{user}/m/{media}/',
requirements=dict(m_id="[0-9a-fA-F]{24}"),
controller="mediagoblin.user_pages.views:media_home"),
+ Route('mediagoblin.user_pages.media_home.view_comment',
+ '/{user}/m/{media}/c/{comment}/',
+ controller="mediagoblin.user_pages.views:media_home"),
Route('mediagoblin.edit.edit_media', "/{user}/m/{media}/edit/",
controller="mediagoblin.edit.views:edit_media"),
Route('mediagoblin.user_pages.atom_feed', '/{user}/atom/',
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 3a8684d3..dc71b059 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -22,8 +22,8 @@ from mediagoblin.util import (
Pagination, render_to_response, redirect, cleaned_markdown_conversion)
from mediagoblin.user_pages import forms as user_forms
-from mediagoblin.decorators import uses_pagination, get_user_media_entry, \
- require_active_login
+from mediagoblin.decorators import (uses_pagination, get_user_media_entry,
+ require_active_login)
from werkzeug.contrib.atom import AtomFeed
@@ -32,10 +32,14 @@ from werkzeug.contrib.atom import AtomFeed
def user_home(request, page):
"""'Homepage' of a User()"""
user = request.db.User.find_one({
- 'username': request.matchdict['user'],
- 'status': 'active'})
+ 'username': request.matchdict['user']})
if not user:
return exc.HTTPNotFound()
+ elif user['status'] != u'active':
+ return render_to_response(
+ request,
+ 'mediagoblin/user_pages/user.html',
+ {'user': user})
cursor = request.db.MediaEntry.find(
{'uploader': user['_id'],
@@ -95,8 +99,14 @@ def media_home(request, media, page, **kwargs):
"""
'Homepage' of a MediaEntry()
"""
+ if ObjectId(request.matchdict.get('comment')):
+ pagination = Pagination(
+ page, media.get_comments(), MEDIA_COMMENTS_PER_PAGE,
+ ObjectId(request.matchdict.get('comment')))
+ else:
+ pagination = Pagination(
+ page, media.get_comments(), MEDIA_COMMENTS_PER_PAGE)
- pagination = Pagination(page, media.get_comments(), MEDIA_COMMENTS_PER_PAGE)
comments = pagination()
comment_form = user_forms.MediaCommentForm(request.POST)
@@ -118,7 +128,7 @@ def media_post_comment(request):
comment = request.db.MediaComment()
comment['media_entry'] = ObjectId(request.matchdict['media'])
comment['author'] = request.user['_id']
- comment['content'] = request.POST['comment']
+ comment['content'] = request.POST['comment_content']
comment['content_html'] = cleaned_markdown_conversion(comment['content'])
diff --git a/mediagoblin/util.py b/mediagoblin/util.py
index f051dc50..bb9f6db4 100644
--- a/mediagoblin/util.py
+++ b/mediagoblin/util.py
@@ -14,6 +14,8 @@
# 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 __future__ import division
+
from email.MIMEText import MIMEText
import gettext
import pkg_resources
@@ -21,8 +23,7 @@ import smtplib
import sys
import re
import urllib
-from math import ceil
-from string import strip
+from math import ceil, floor
import copy
import wtforms
@@ -37,6 +38,10 @@ from mediagoblin import mg_globals
from mediagoblin import messages
from mediagoblin.db.util import ObjectId
+from itertools import izip, count
+
+DISPLAY_IMAGE_FETCHING_ORDER = [u'medium', u'original', u'thumb']
+
TESTS_ENABLED = False
def _activate_testing():
"""
@@ -135,7 +140,16 @@ def render_to_response(request, template, context):
def redirect(request, *args, **kwargs):
"""Returns a HTTPFound(), takes a request and then urlgen params"""
- return exc.HTTPFound(location=request.urlgen(*args, **kwargs))
+
+ querystring = None
+ if kwargs.get('querystring'):
+ querystring = kwargs.get('querystring')
+ del kwargs['querystring']
+
+ return exc.HTTPFound(
+ location=''.join([
+ request.urlgen(*args, **kwargs),
+ querystring if querystring else '']))
def setup_user_in_request(request):
@@ -254,9 +268,9 @@ def send_email(from_addr, to_addrs, subject, message_body):
- message_body: email body text
"""
# TODO: make a mock mhost if testing is enabled
- if TESTS_ENABLED or mg_globals.email_debug_mode:
+ if TESTS_ENABLED or mg_globals.app_config['email_debug_mode']:
mhost = FakeMhost()
- elif not mg_globals.email_debug_mode:
+ elif not mg_globals.app_config['email_debug_mode']:
mhost = smtplib.SMTP()
mhost.connect()
@@ -269,7 +283,7 @@ def send_email(from_addr, to_addrs, subject, message_body):
if TESTS_ENABLED:
EMAIL_TEST_INBOX.append(message)
- if getattr(mg_globals, 'email_debug_mode', False):
+ if mg_globals.app_config['email_debug_mode']:
print u"===== Email ====="
print u"From address: %s" % message['From']
print u"To addresses: %s" % message['To']
@@ -478,7 +492,8 @@ class Pagination(object):
get actual data slice through __call__().
"""
- def __init__(self, page, cursor, per_page=PAGINATION_DEFAULT_PER_PAGE):
+ def __init__(self, page, cursor, per_page=PAGINATION_DEFAULT_PER_PAGE,
+ jump_to_id=False):
"""
Initializes Pagination
@@ -486,11 +501,25 @@ class Pagination(object):
- page: requested page
- per_page: number of objects per page
- cursor: db cursor
+ - jump_to_id: ObjectId, sets the page to the page containing the object
+ with _id == jump_to_id.
"""
- self.page = page
+ self.page = page
self.per_page = per_page
self.cursor = cursor
self.total_count = self.cursor.count()
+ self.active_id = None
+
+ if jump_to_id:
+ cursor = copy.copy(self.cursor)
+
+ for (doc, increment) in izip(cursor, count(0)):
+ if doc['_id'] == jump_to_id:
+ self.page = 1 + int(floor(increment / self.per_page))
+
+ self.active_id = jump_to_id
+ break
+
def __call__(self):
"""
diff --git a/mediagoblin/views.py b/mediagoblin/views.py
index 5b6d9773..e7d9dbdd 100644
--- a/mediagoblin/views.py
+++ b/mediagoblin/views.py
@@ -14,6 +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 import mg_globals
from mediagoblin.util import render_to_response
from mediagoblin.db.util import DESCENDING
@@ -23,7 +24,8 @@ def root_view(request):
return render_to_response(
request, 'mediagoblin/root.html',
- {'media_entries': media_entries})
+ {'media_entries': media_entries,
+ 'allow_registration': mg_globals.app_config["allow_registration"]})
def simple_template_render(request):