aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin
diff options
context:
space:
mode:
Diffstat (limited to 'mediagoblin')
-rw-r--r--mediagoblin/app.py65
-rw-r--r--mediagoblin/auth/lib.py2
-rw-r--r--mediagoblin/auth/views.py33
-rw-r--r--mediagoblin/config_spec.ini7
-rw-r--r--mediagoblin/contrib/960_12_col.css357
-rw-r--r--mediagoblin/contrib/960_16_col.css447
-rw-r--r--mediagoblin/contrib/reset.css202
-rw-r--r--mediagoblin/contrib/text.css86
-rw-r--r--mediagoblin/db/__init__.py46
-rw-r--r--mediagoblin/db/indexes.py10
-rw-r--r--mediagoblin/db/migrations.py61
-rw-r--r--mediagoblin/db/models.py60
-rw-r--r--mediagoblin/db/open.py36
-rw-r--r--mediagoblin/db/util.py198
-rw-r--r--mediagoblin/edit/forms.py6
-rw-r--r--mediagoblin/edit/views.py46
-rw-r--r--mediagoblin/gmg_commands/migrate.py44
-rw-r--r--mediagoblin/init/__init__.py82
-rw-r--r--mediagoblin/init/celery/__init__.py (renamed from mediagoblin/celery_setup/__init__.py)4
-rw-r--r--mediagoblin/init/celery/dummy_settings_module.py (renamed from mediagoblin/celery_setup/dummy_settings_module.py)0
-rw-r--r--mediagoblin/init/celery/from_celery.py (renamed from mediagoblin/celery_setup/from_celery.py)2
-rw-r--r--mediagoblin/init/celery/from_tests.py (renamed from mediagoblin/celery_setup/from_tests.py)2
-rw-r--r--mediagoblin/init/config.py (renamed from mediagoblin/config.py)0
-rw-r--r--mediagoblin/mg_globals.py6
-rw-r--r--mediagoblin/process_media/__init__.py6
-rw-r--r--mediagoblin/static/css/base.css206
l---------mediagoblin/static/css/contrib/960_12_col.css1
l---------mediagoblin/static/css/contrib/960_16_col.css1
l---------mediagoblin/static/css/contrib/reset.css1
l---------mediagoblin/static/css/contrib/text.css1
-rw-r--r--mediagoblin/static/images/icon.pngbin1670 -> 0 bytes
-rw-r--r--mediagoblin/static/images/icon_delete.pngbin0 -> 472 bytes
-rw-r--r--mediagoblin/static/images/icon_edit.pngbin0 -> 297 bytes
-rw-r--r--mediagoblin/static/images/icon_feed.pngbin0 -> 522 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/routing.py5
-rw-r--r--mediagoblin/submit/views.py11
-rw-r--r--mediagoblin/templates/mediagoblin/auth/login.html7
-rw-r--r--mediagoblin/templates/mediagoblin/auth/register.html2
-rw-r--r--mediagoblin/templates/mediagoblin/auth/verify_email.html28
-rw-r--r--mediagoblin/templates/mediagoblin/base.html26
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit.html2
-rw-r--r--mediagoblin/templates/mediagoblin/edit/edit_profile.html6
-rw-r--r--mediagoblin/templates/mediagoblin/media_details.html37
-rw-r--r--mediagoblin/templates/mediagoblin/root.html11
-rw-r--r--mediagoblin/templates/mediagoblin/submit/start.html7
-rw-r--r--mediagoblin/templates/mediagoblin/submit/success.html22
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/gallery.html6
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/media.html116
-rw-r--r--mediagoblin/templates/mediagoblin/user_pages/user.html40
-rw-r--r--mediagoblin/templates/mediagoblin/utils/object_gallery.html34
-rw-r--r--mediagoblin/templates/mediagoblin/utils/pagination.html65
-rw-r--r--mediagoblin/templates/mediagoblin/utils/prev_next.html46
-rw-r--r--mediagoblin/templates/mediagoblin/utils/profile.html27
-rw-r--r--mediagoblin/tests/test_auth.py4
-rw-r--r--mediagoblin/tests/test_celery_setup.py4
-rw-r--r--mediagoblin/tests/test_config.py2
-rw-r--r--mediagoblin/tests/test_mgoblin_app.ini2
-rw-r--r--mediagoblin/tests/test_migrations.py402
-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.py42
-rw-r--r--mediagoblin/user_pages/forms.py43
-rw-r--r--mediagoblin/user_pages/routing.py3
-rw-r--r--mediagoblin/user_pages/views.py21
-rw-r--r--mediagoblin/util.py58
-rw-r--r--mediagoblin/views.py4
76 files changed, 2327 insertions, 931 deletions
diff --git a/mediagoblin/app.py b/mediagoblin/app.py
index b27b5761..1c38f778 100644
--- a/mediagoblin/app.py
+++ b/mediagoblin/app.py
@@ -20,17 +20,16 @@ import urllib
import routes
from webob import Request, exc
-from mediagoblin import routing, util, storage, staticdirect
-from mediagoblin.config import (
- read_mediagoblin_config, generate_validation_report)
+from mediagoblin import routing, util, storage
from mediagoblin.db.open import setup_connection_and_db_from_config
+from mediagoblin.db.util import MigrationManager
from mediagoblin.mg_globals import setup_globals
-from mediagoblin.celery_setup import setup_celery_from_config
-from mediagoblin.workbench import WorkbenchManager
+from mediagoblin.init.celery import setup_celery_from_config
+from mediagoblin.init import get_jinja_loader, get_staticdirector, \
+ setup_global_and_app_config, setup_workbench
-
-class Error(Exception): pass
-class ImproperlyConfigured(Error): pass
+# This MUST be imported so as to set up the appropriate migrations!
+from mediagoblin.db import migrations
class MediaGoblinApp(object):
@@ -54,13 +53,7 @@ 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
@@ -70,8 +63,18 @@ class MediaGoblinApp(object):
self.connection, self.db = setup_connection_and_db_from_config(
app_config)
+ # Init the migration number if necessary
+ migration_manager = MigrationManager(self.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():
+ print (
+ "*WARNING:* Your migrations are out of date, "
+ "maybe run ./bin/gmg migrate?")
+
# Get the template environment
- self.template_loader = util.get_jinja_loader(
+ self.template_loader = get_jinja_loader(
app_config.get('user_template_path'))
# Set up storage systems
@@ -84,19 +87,7 @@ class MediaGoblinApp(object):
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,21 +107,15 @@ class MediaGoblinApp(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']))
+ queue_store=self.queue_store)
+
+ # 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/views.py b/mediagoblin/auth/views.py
index 1d00f382..7fe507b1 100644
--- a/mediagoblin/auth/views.py
+++ b/mediagoblin/auth/views.py
@@ -18,6 +18,8 @@ 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
@@ -29,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():
@@ -51,7 +61,7 @@ def register(request):
entry['pw_hash'] = auth_lib.bcrypt_gen_password_hash(
request.POST['password'])
entry.save(validate=True)
-
+
send_verification_email(entry, request)
return redirect(request, "mediagoblin.auth.register_success")
@@ -97,13 +107,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")
@@ -124,16 +135,24 @@ def verify_email(request):
if user and user['verification_key'] == unicode(request.GET['token']):
user['status'] = u'active'
user['email_verified'] = True
- verification_successful = True
user.save()
+ verification_successful = True
+ messages.add_message(
+ 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')
+
return render_to_response(
request,
- 'mediagoblin/auth/verify_email.html',
+ 'mediagoblin/user_pages/user.html',
{'user': user,
- 'verification_successful': verification_successful})
+ 'verification_successful' : verification_successful})
def resend_activation(request):
diff --git a/mediagoblin/config_spec.ini b/mediagoblin/config_spec.ini
index aadf5c21..28be5f34 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)
+
# By default not set, but you might want something like:
# "%(here)s/user_dev/templates/"
local_templates = string()
@@ -73,4 +76,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/contrib/960_12_col.css b/mediagoblin/contrib/960_12_col.css
deleted file mode 100644
index 48e86ee8..00000000
--- a/mediagoblin/contrib/960_12_col.css
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- 960 Grid System ~ Core CSS.
- Learn more ~ http://960.gs/
-
- Licensed under GPL and MIT.
-*/
-
-/*
- Forces backgrounds to span full width,
- even if there is horizontal scrolling.
- Increase this if your layout is wider.
-
- Note: IE6 works fine without this fix.
-*/
-
-body {
- min-width: 960px;
-}
-
-/* `Container
-----------------------------------------------------------------------------------------------------*/
-
-.container_12 {
- margin-left: auto;
- margin-right: auto;
- width: 960px;
-}
-
-/* `Grid >> Global
-----------------------------------------------------------------------------------------------------*/
-
-.grid_1,
-.grid_2,
-.grid_3,
-.grid_4,
-.grid_5,
-.grid_6,
-.grid_7,
-.grid_8,
-.grid_9,
-.grid_10,
-.grid_11,
-.grid_12 {
- display: inline;
- float: left;
- margin-left: 10px;
- margin-right: 10px;
-}
-
-.push_1, .pull_1,
-.push_2, .pull_2,
-.push_3, .pull_3,
-.push_4, .pull_4,
-.push_5, .pull_5,
-.push_6, .pull_6,
-.push_7, .pull_7,
-.push_8, .pull_8,
-.push_9, .pull_9,
-.push_10, .pull_10,
-.push_11, .pull_11 {
- position: relative;
-}
-
-/* `Grid >> Children (Alpha ~ First, Omega ~ Last)
-----------------------------------------------------------------------------------------------------*/
-
-.alpha {
- margin-left: 0;
-}
-
-.omega {
- margin-right: 0;
-}
-
-/* `Grid >> 12 Columns
-----------------------------------------------------------------------------------------------------*/
-
-.container_12 .grid_1 {
- width: 60px;
-}
-
-.container_12 .grid_2 {
- width: 140px;
-}
-
-.container_12 .grid_3 {
- width: 220px;
-}
-
-.container_12 .grid_4 {
- width: 300px;
-}
-
-.container_12 .grid_5 {
- width: 380px;
-}
-
-.container_12 .grid_6 {
- width: 460px;
-}
-
-.container_12 .grid_7 {
- width: 540px;
-}
-
-.container_12 .grid_8 {
- width: 620px;
-}
-
-.container_12 .grid_9 {
- width: 700px;
-}
-
-.container_12 .grid_10 {
- width: 780px;
-}
-
-.container_12 .grid_11 {
- width: 860px;
-}
-
-.container_12 .grid_12 {
- width: 940px;
-}
-
-/* `Prefix Extra Space >> 12 Columns
-----------------------------------------------------------------------------------------------------*/
-
-.container_12 .prefix_1 {
- padding-left: 80px;
-}
-
-.container_12 .prefix_2 {
- padding-left: 160px;
-}
-
-.container_12 .prefix_3 {
- padding-left: 240px;
-}
-
-.container_12 .prefix_4 {
- padding-left: 320px;
-}
-
-.container_12 .prefix_5 {
- padding-left: 400px;
-}
-
-.container_12 .prefix_6 {
- padding-left: 480px;
-}
-
-.container_12 .prefix_7 {
- padding-left: 560px;
-}
-
-.container_12 .prefix_8 {
- padding-left: 640px;
-}
-
-.container_12 .prefix_9 {
- padding-left: 720px;
-}
-
-.container_12 .prefix_10 {
- padding-left: 800px;
-}
-
-.container_12 .prefix_11 {
- padding-left: 880px;
-}
-
-/* `Suffix Extra Space >> 12 Columns
-----------------------------------------------------------------------------------------------------*/
-
-.container_12 .suffix_1 {
- padding-right: 80px;
-}
-
-.container_12 .suffix_2 {
- padding-right: 160px;
-}
-
-.container_12 .suffix_3 {
- padding-right: 240px;
-}
-
-.container_12 .suffix_4 {
- padding-right: 320px;
-}
-
-.container_12 .suffix_5 {
- padding-right: 400px;
-}
-
-.container_12 .suffix_6 {
- padding-right: 480px;
-}
-
-.container_12 .suffix_7 {
- padding-right: 560px;
-}
-
-.container_12 .suffix_8 {
- padding-right: 640px;
-}
-
-.container_12 .suffix_9 {
- padding-right: 720px;
-}
-
-.container_12 .suffix_10 {
- padding-right: 800px;
-}
-
-.container_12 .suffix_11 {
- padding-right: 880px;
-}
-
-/* `Push Space >> 12 Columns
-----------------------------------------------------------------------------------------------------*/
-
-.container_12 .push_1 {
- left: 80px;
-}
-
-.container_12 .push_2 {
- left: 160px;
-}
-
-.container_12 .push_3 {
- left: 240px;
-}
-
-.container_12 .push_4 {
- left: 320px;
-}
-
-.container_12 .push_5 {
- left: 400px;
-}
-
-.container_12 .push_6 {
- left: 480px;
-}
-
-.container_12 .push_7 {
- left: 560px;
-}
-
-.container_12 .push_8 {
- left: 640px;
-}
-
-.container_12 .push_9 {
- left: 720px;
-}
-
-.container_12 .push_10 {
- left: 800px;
-}
-
-.container_12 .push_11 {
- left: 880px;
-}
-
-/* `Pull Space >> 12 Columns
-----------------------------------------------------------------------------------------------------*/
-
-.container_12 .pull_1 {
- left: -80px;
-}
-
-.container_12 .pull_2 {
- left: -160px;
-}
-
-.container_12 .pull_3 {
- left: -240px;
-}
-
-.container_12 .pull_4 {
- left: -320px;
-}
-
-.container_12 .pull_5 {
- left: -400px;
-}
-
-.container_12 .pull_6 {
- left: -480px;
-}
-
-.container_12 .pull_7 {
- left: -560px;
-}
-
-.container_12 .pull_8 {
- left: -640px;
-}
-
-.container_12 .pull_9 {
- left: -720px;
-}
-
-.container_12 .pull_10 {
- left: -800px;
-}
-
-.container_12 .pull_11 {
- left: -880px;
-}
-
-/* `Clear Floated Elements
-----------------------------------------------------------------------------------------------------*/
-
-/* http://sonspring.com/journal/clearing-floats */
-
-.clear {
- clear: both;
- display: block;
- overflow: hidden;
- visibility: hidden;
- width: 0;
- height: 0;
-}
-
-/* http://www.yuiblog.com/blog/2010/09/27/clearfix-reloaded-overflowhidden-demystified */
-
-.clearfix:before,
-.clearfix:after,
-.container_12:before,
-.container_12:after {
- content: '.';
- display: block;
- overflow: hidden;
- visibility: hidden;
- font-size: 0;
- line-height: 0;
- width: 0;
- height: 0;
-}
-
-.clearfix:after,
-.container_12:after {
- clear: both;
-}
-
-/*
- The following zoom:1 rule is specifically for IE6 + IE7.
- Move to separate stylesheet if invalid CSS is a problem.
-*/
-
-.clearfix,
-.container_12 {
- zoom: 1;
-} \ No newline at end of file
diff --git a/mediagoblin/contrib/960_16_col.css b/mediagoblin/contrib/960_16_col.css
new file mode 100644
index 00000000..faa6d8b2
--- /dev/null
+++ b/mediagoblin/contrib/960_16_col.css
@@ -0,0 +1,447 @@
+/*
+ 960 Grid System ~ Core CSS.
+ Learn more ~ http://960.gs/
+
+ Licensed under GPL and MIT.
+*/
+
+/*
+ Forces backgrounds to span full width,
+ even if there is horizontal scrolling.
+ Increase this if your layout is wider.
+
+ Note: IE6 works fine without this fix.
+*/
+
+body {
+ min-width: 960px;
+}
+
+/* Container
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 {
+ margin-left: auto;
+ margin-right: auto;
+ width: 960px;
+}
+
+/* Grid >> Global
+----------------------------------------------------------------------------------------------------*/
+
+.grid_1,
+.grid_2,
+.grid_3,
+.grid_4,
+.grid_5,
+.grid_6,
+.grid_7,
+.grid_8,
+.grid_9,
+.grid_10,
+.grid_11,
+.grid_12,
+.grid_13,
+.grid_14,
+.grid_15,
+.grid_16 {
+ display: inline;
+ float: left;
+ position: relative;
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+.push_1, .pull_1,
+.push_2, .pull_2,
+.push_3, .pull_3,
+.push_4, .pull_4,
+.push_5, .pull_5,
+.push_6, .pull_6,
+.push_7, .pull_7,
+.push_8, .pull_8,
+.push_9, .pull_9,
+.push_10, .pull_10,
+.push_11, .pull_11,
+.push_12, .pull_12,
+.push_13, .pull_13,
+.push_14, .pull_14,
+.push_15, .pull_15,
+.push_16, .pull_16 {
+ position: relative;
+}
+
+/* Grid >> Children (Alpha ~ First, Omega ~ Last)
+----------------------------------------------------------------------------------------------------*/
+
+.alpha {
+ margin-left: 0;
+}
+
+.omega {
+ margin-right: 0;
+}
+
+/* Grid >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .grid_1 {
+ width: 40px;
+}
+
+.container_16 .grid_2 {
+ width: 100px;
+}
+
+.container_16 .grid_3 {
+ width: 160px;
+}
+
+.container_16 .grid_4 {
+ width: 220px;
+}
+
+.container_16 .grid_5 {
+ width: 280px;
+}
+
+.container_16 .grid_6 {
+ width: 340px;
+}
+
+.container_16 .grid_7 {
+ width: 400px;
+}
+
+.container_16 .grid_8 {
+ width: 460px;
+}
+
+.container_16 .grid_9 {
+ width: 520px;
+}
+
+.container_16 .grid_10 {
+ width: 580px;
+}
+
+.container_16 .grid_11 {
+ width: 640px;
+}
+
+.container_16 .grid_12 {
+ width: 700px;
+}
+
+.container_16 .grid_13 {
+ width: 760px;
+}
+
+.container_16 .grid_14 {
+ width: 820px;
+}
+
+.container_16 .grid_15 {
+ width: 880px;
+}
+
+.container_16 .grid_16 {
+ width: 940px;
+}
+
+/* Prefix Extra Space >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .prefix_1 {
+ padding-left: 60px;
+}
+
+.container_16 .prefix_2 {
+ padding-left: 120px;
+}
+
+.container_16 .prefix_3 {
+ padding-left: 180px;
+}
+
+.container_16 .prefix_4 {
+ padding-left: 240px;
+}
+
+.container_16 .prefix_5 {
+ padding-left: 300px;
+}
+
+.container_16 .prefix_6 {
+ padding-left: 360px;
+}
+
+.container_16 .prefix_7 {
+ padding-left: 420px;
+}
+
+.container_16 .prefix_8 {
+ padding-left: 480px;
+}
+
+.container_16 .prefix_9 {
+ padding-left: 540px;
+}
+
+.container_16 .prefix_10 {
+ padding-left: 600px;
+}
+
+.container_16 .prefix_11 {
+ padding-left: 660px;
+}
+
+.container_16 .prefix_12 {
+ padding-left: 720px;
+}
+
+.container_16 .prefix_13 {
+ padding-left: 780px;
+}
+
+.container_16 .prefix_14 {
+ padding-left: 840px;
+}
+
+.container_16 .prefix_15 {
+ padding-left: 900px;
+}
+
+/* Suffix Extra Space >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .suffix_1 {
+ padding-right: 60px;
+}
+
+.container_16 .suffix_2 {
+ padding-right: 120px;
+}
+
+.container_16 .suffix_3 {
+ padding-right: 180px;
+}
+
+.container_16 .suffix_4 {
+ padding-right: 240px;
+}
+
+.container_16 .suffix_5 {
+ padding-right: 300px;
+}
+
+.container_16 .suffix_6 {
+ padding-right: 360px;
+}
+
+.container_16 .suffix_7 {
+ padding-right: 420px;
+}
+
+.container_16 .suffix_8 {
+ padding-right: 480px;
+}
+
+.container_16 .suffix_9 {
+ padding-right: 540px;
+}
+
+.container_16 .suffix_10 {
+ padding-right: 600px;
+}
+
+.container_16 .suffix_11 {
+ padding-right: 660px;
+}
+
+.container_16 .suffix_12 {
+ padding-right: 720px;
+}
+
+.container_16 .suffix_13 {
+ padding-right: 780px;
+}
+
+.container_16 .suffix_14 {
+ padding-right: 840px;
+}
+
+.container_16 .suffix_15 {
+ padding-right: 900px;
+}
+
+/* Push Space >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .push_1 {
+ left: 60px;
+}
+
+.container_16 .push_2 {
+ left: 120px;
+}
+
+.container_16 .push_3 {
+ left: 180px;
+}
+
+.container_16 .push_4 {
+ left: 240px;
+}
+
+.container_16 .push_5 {
+ left: 300px;
+}
+
+.container_16 .push_6 {
+ left: 360px;
+}
+
+.container_16 .push_7 {
+ left: 420px;
+}
+
+.container_16 .push_8 {
+ left: 480px;
+}
+
+.container_16 .push_9 {
+ left: 540px;
+}
+
+.container_16 .push_10 {
+ left: 600px;
+}
+
+.container_16 .push_11 {
+ left: 660px;
+}
+
+.container_16 .push_12 {
+ left: 720px;
+}
+
+.container_16 .push_13 {
+ left: 780px;
+}
+
+.container_16 .push_14 {
+ left: 840px;
+}
+
+.container_16 .push_15 {
+ left: 900px;
+}
+
+/* Pull Space >> 16 Columns
+----------------------------------------------------------------------------------------------------*/
+
+.container_16 .pull_1 {
+ left: -60px;
+}
+
+.container_16 .pull_2 {
+ left: -120px;
+}
+
+.container_16 .pull_3 {
+ left: -180px;
+}
+
+.container_16 .pull_4 {
+ left: -240px;
+}
+
+.container_16 .pull_5 {
+ left: -300px;
+}
+
+.container_16 .pull_6 {
+ left: -360px;
+}
+
+.container_16 .pull_7 {
+ left: -420px;
+}
+
+.container_16 .pull_8 {
+ left: -480px;
+}
+
+.container_16 .pull_9 {
+ left: -540px;
+}
+
+.container_16 .pull_10 {
+ left: -600px;
+}
+
+.container_16 .pull_11 {
+ left: -660px;
+}
+
+.container_16 .pull_12 {
+ left: -720px;
+}
+
+.container_16 .pull_13 {
+ left: -780px;
+}
+
+.container_16 .pull_14 {
+ left: -840px;
+}
+
+.container_16 .pull_15 {
+ left: -900px;
+}
+
+/* `Clear Floated Elements
+----------------------------------------------------------------------------------------------------*/
+
+/* http://sonspring.com/journal/clearing-floats */
+
+.clear {
+ clear: both;
+ display: block;
+ overflow: hidden;
+ visibility: hidden;
+ width: 0;
+ height: 0;
+}
+
+/* http://www.yuiblog.com/blog/2010/09/27/clearfix-reloaded-overflowhidden-demystified */
+
+.clearfix:before,
+.clearfix:after,
+.container_16:before,
+.container_16:after {
+ content: '.';
+ display: block;
+ overflow: hidden;
+ visibility: hidden;
+ font-size: 0;
+ line-height: 0;
+ width: 0;
+ height: 0;
+}
+
+.clearfix:after,
+.container_16:after {
+ clear: both;
+}
+
+/*
+ The following zoom:1 rule is specifically for IE6 + IE7.
+ Move to separate stylesheet if invalid CSS is a problem.
+*/
+
+.clearfix,
+.container_16 {
+ zoom: 1;
+} \ No newline at end of file
diff --git a/mediagoblin/contrib/reset.css b/mediagoblin/contrib/reset.css
new file mode 100644
index 00000000..87b7f368
--- /dev/null
+++ b/mediagoblin/contrib/reset.css
@@ -0,0 +1,202 @@
+/* `XHTML, HTML4, HTML5 Reset
+----------------------------------------------------------------------------------------------------*/
+
+a,
+abbr,
+acronym,
+address,
+applet,
+article,
+aside,
+audio,
+b,
+big,
+blockquote,
+body,
+canvas,
+caption,
+center,
+cite,
+code,
+dd,
+del,
+details,
+dfn,
+dialog,
+div,
+dl,
+dt,
+em,
+embed,
+fieldset,
+figcaption,
+figure,
+font,
+footer,
+form,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+header,
+hgroup,
+hr,
+html,
+i,
+iframe,
+img,
+ins,
+kbd,
+label,
+legend,
+li,
+mark,
+menu,
+meter,
+nav,
+object,
+ol,
+output,
+p,
+pre,
+progress,
+q,
+rp,
+rt,
+ruby,
+s,
+samp,
+section,
+small,
+span,
+strike,
+strong,
+sub,
+summary,
+sup,
+table,
+tbody,
+td,
+tfoot,
+th,
+thead,
+time,
+tr,
+tt,
+u,
+ul,
+var,
+video,
+xmp {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ font-size: 100%;
+}
+
+html,
+body {
+ height: 100%;
+}
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+/*
+ Override the default (display: inline) for
+ browsers that do not recognize HTML5 tags.
+
+ IE8 (and lower) requires a shiv:
+ http://ejohn.org/blog/html5-shiv
+*/
+ display: block;
+}
+
+b,
+strong {
+/*
+ Makes browsers agree.
+ IE + Opera = font-weight: bold.
+ Gecko + WebKit = font-weight: bolder.
+*/
+ font-weight: bold;
+}
+
+img {
+ color: transparent;
+ font-size: 0;
+ vertical-align: middle;
+/*
+ For IE.
+ http://css-tricks.com/ie-fix-bicubic-scaling-for-images
+*/
+ -ms-interpolation-mode: bicubic;
+}
+
+li {
+/*
+ For IE6 + IE7.
+*/
+ display: list-item;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+th,
+td,
+caption {
+ font-weight: normal;
+ vertical-align: top;
+ text-align: left;
+}
+
+q {
+ quotes: none;
+}
+
+q:before,
+q:after {
+ content: '';
+ content: none;
+}
+
+sub,
+sup,
+small {
+ font-size: 75%;
+}
+
+sub,
+sup {
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+svg {
+/*
+ For IE9.
+*/
+ overflow: hidden;
+} \ No newline at end of file
diff --git a/mediagoblin/contrib/text.css b/mediagoblin/contrib/text.css
new file mode 100644
index 00000000..1a6b302f
--- /dev/null
+++ b/mediagoblin/contrib/text.css
@@ -0,0 +1,86 @@
+/*
+ 960 Grid System ~ Text CSS.
+ Learn more ~ http://960.gs/
+
+ Licensed under GPL and MIT.
+*/
+
+/* `Basic HTML
+----------------------------------------------------------------------------------------------------*/
+
+body {
+ font: 13px/1.5 'Helvetica Neue', Arial, 'Liberation Sans', FreeSans, sans-serif;
+}
+
+pre,
+code {
+ font-family: 'DejaVu Sans Mono', Monaco, Consolas, monospace;
+}
+
+hr {
+ border: 0 #ccc solid;
+ border-top-width: 1px;
+ clear: both;
+ height: 0;
+}
+
+/* `Headings
+----------------------------------------------------------------------------------------------------*/
+
+h1 {
+ font-size: 25px;
+}
+
+h2 {
+ font-size: 23px;
+}
+
+h3 {
+ font-size: 21px;
+}
+
+h4 {
+ font-size: 19px;
+}
+
+h5 {
+ font-size: 17px;
+}
+
+h6 {
+ font-size: 15px;
+}
+
+/* `Spacing
+----------------------------------------------------------------------------------------------------*/
+
+ol {
+ list-style: decimal;
+}
+
+ul {
+ list-style: disc;
+}
+
+li {
+ margin-left: 30px;
+}
+
+p,
+dl,
+hr,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+ol,
+ul,
+pre,
+table,
+address,
+fieldset,
+figure {
+ margin-bottom: 20px;
+} \ No newline at end of file
diff --git a/mediagoblin/db/__init__.py b/mediagoblin/db/__init__.py
index c129cbf8..776025ca 100644
--- a/mediagoblin/db/__init__.py
+++ b/mediagoblin/db/__init__.py
@@ -13,3 +13,49 @@
#
# 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/>.
+
+"""
+Database Abstraction/Wrapper Layer
+==================================
+
+ **NOTE from Chris Webber:** I asked Elrond to explain why he put
+ ASCENDING and DESCENDING in db/util.py when we could just import from
+ pymongo. Read beow for why, but note that nobody is actually doing
+ this and there's no proof that we'll ever support more than
+ MongoDB... it would be a huge amount of work to do so.
+
+ If you really want to prove that possible, jump on IRC and talk to
+ us about making such a branch. In the meanwhile, it doesn't hurt to
+ have things as they are... if it ever makes it hard for us to
+ actually do things, we might revisit or remove this. But for more
+ information, read below.
+
+This submodule is for most of the db specific stuff.
+
+There are two main ideas here:
+
+1. Open up a small possibility to replace mongo by another
+ db. This means, that all direct mongo accesses should
+ happen in the db submodule. While all the rest uses an
+ API defined by this submodule.
+
+ Currently this API happens to be basicly mongo.
+ Which means, that the abstraction/wrapper layer is
+ extremely thin.
+
+2. Give the rest of the app a simple and easy way to get most of
+ their db needs. Which often means some simple import
+ from db.util.
+
+What does that mean?
+
+* Never import mongo directly outside of this submodule.
+
+* Inside this submodule you can do whatever is needed. The
+ API border is exactly at the submodule layer. Nowhere
+ else.
+
+* helper functions can be moved in here. They become part
+ of the db.* API
+
+"""
diff --git a/mediagoblin/db/indexes.py b/mediagoblin/db/indexes.py
index d379a52b..a832e013 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 b888ad3e..f398f4b3 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -14,56 +14,25 @@
# 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'])}}
+ target = collection.find(
+ {'bio_html': {'$exists': False}})
-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']
+ for document in target:
+ document['bio_html'] = cleaned_markdown_conversion(
+ document['bio'])
+ collection.save(document)
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index 2d6a71f7..9d7bcf6b 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -16,16 +16,17 @@
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 DESCENDING, ObjectId
+from mediagoblin.db.util import ASCENDING, DESCENDING, ObjectId
from mediagoblin.util import Pagination
from mediagoblin.util import DISPLAY_IMAGE_FETCHING_ORDER
+
###################
# Custom validators
###################
@@ -49,7 +50,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']
@@ -61,8 +63,6 @@ class User(Document):
'verification_key': lambda: unicode(uuid.uuid4()),
'is_admin': False}
- migration_handler = migrations.UserMigration
-
def check_login(self, password):
"""
See if a user can login with this password
@@ -108,7 +108,9 @@ 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):
"""
@@ -130,22 +132,7 @@ class MediaEntry(Document):
def main_mediafile(self):
pass
-
- def get_comments(self, page):
- cursor = self.db.MediaComment.find({
- 'media_entry': self['_id']}).sort('created', DESCENDING)
-
- pagination = Pagination(page, cursor)
- comments = pagination()
-
- data = list()
- for comment in comments:
- comment['author'] = self.db.User.find_one({
- '_id': comment['author']})
- data.append(comment)
-
- return (data, pagination)
-
+
def generate_slug(self):
self['slug'] = util.slugify(self['title'])
@@ -173,10 +160,38 @@ 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']},
+ 'uploader': self['uploader'],
+ 'state': 'processed'}).sort(
+ '_id', ASCENDING).limit(1)
+ if cursor.count():
+ 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']},
+ 'uploader': self['uploader'],
+ 'state': 'processed'}).sort(
+ '_id', DESCENDING).limit(1)
+
+ if cursor.count():
+ return urlgen('mediagoblin.user_pages.media_home',
+ user=self.uploader()['username'],
+ media=unicode(cursor[0]['slug']))
def uploader(self):
return self.db.User.find_one({'_id': self['uploader']})
+
class MediaComment(Document):
__collection__ = 'media_comments'
@@ -199,6 +214,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 46f899f7..0f3220d2 100644
--- a/mediagoblin/db/util.py
+++ b/mediagoblin/db/util.py
@@ -30,13 +30,18 @@ document relevant to here:
import copy
# Imports that other modules might use
-from pymongo import DESCENDING
+from pymongo import ASCENDING, DESCENDING
from pymongo.errors import InvalidId
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/edit/forms.py b/mediagoblin/edit/forms.py
index 2efdb9e4..0ed52af1 100644
--- a/mediagoblin/edit/forms.py
+++ b/mediagoblin/edit/forms.py
@@ -23,7 +23,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')
class EditProfileForm(wtforms.Form):
@@ -31,4 +32,5 @@ class EditProfileForm(wtforms.Form):
[wtforms.validators.Length(min=0, max=500)])
url = wtforms.TextField(
'Website',
- [wtforms.validators.URL(message='Improperly formed URL')])
+ [wtforms.validators.Optional(),
+ wtforms.validators.URL(message='Improperly formed URL')])
diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py
index a9071495..f372fbb9 100644
--- a/mediagoblin/edit/views.py
+++ b/mediagoblin/edit/views.py
@@ -17,13 +17,13 @@
from webob import exc
-from mediagoblin.util import render_to_response, redirect, clean_html
+from mediagoblin import messages
+from mediagoblin.util import (
+ render_to_response, redirect, 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
@@ -50,12 +50,9 @@ def edit_media(request, media):
else:
media['title'] = request.POST['title']
media['description'] = request.POST.get('description')
-
- 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()
@@ -63,6 +60,14 @@ def edit_media(request, media):
return redirect(request, "mediagoblin.user_pages.media_home",
user=media.uploader()['username'], media=media['slug'])
+ if request.user['is_admin'] \
+ and media['uploader'] != request.user['_id'] \
+ and request.method != 'POST':
+ messages.add_message(
+ request, messages.WARNING,
+ "You are editing another user's media. Proceed with caution.")
+
+
return render_to_response(
request,
'mediagoblin/edit/edit.html',
@@ -73,7 +78,18 @@ def edit_media(request, media):
@require_active_login
def edit_profile(request):
- user = request.user
+ # admins may edit any user profile given a username in the querystring
+ edit_username = request.GET.get('username')
+ if request.user['is_admin'] and request.user['username'] != edit_username:
+ user = request.db.User.find_one({'username': edit_username})
+ # No need to warn again if admin just submitted an edited profile
+ if request.method != 'POST':
+ messages.add_message(
+ request, messages.WARNING,
+ "You are editing a user's profile. Proceed with caution.")
+ else:
+ user = request.user
+
form = forms.EditProfileForm(request.POST,
url = user.get('url'),
bio = user.get('bio'))
@@ -81,9 +97,17 @@ 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()
- return redirect(request, "index", user=user['username'])
+ messages.add_message(request,
+ messages.SUCCESS,
+ 'Profile edited!')
+ return redirect(request,
+ 'mediagoblin.user_pages.user_home',
+ user=edit_username)
return render_to_response(
request,
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/init/__init__.py b/mediagoblin/init/__init__.py
new file mode 100644
index 00000000..6320d21b
--- /dev/null
+++ b/mediagoblin/init/__init__.py
@@ -0,0 +1,82 @@
+# 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 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.workbench import WorkbenchManager
+
+
+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 get_jinja_loader(user_template_path=None):
+ """
+ Set up the Jinja template loaders, possibly allowing for user
+ overridden templates.
+
+ (In the future we may have another system for providing theming;
+ for now this is good enough.)
+ """
+ if user_template_path:
+ return jinja2.ChoiceLoader(
+ [jinja2.FileSystemLoader(user_template_path),
+ 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_workbench():
+ app_config = mg_globals.app_config
+
+ workbench_manager = WorkbenchManager(app_config['workbench_path'])
+
+ setup_globals(workbench_manager = workbench_manager)
diff --git a/mediagoblin/celery_setup/__init__.py b/mediagoblin/init/celery/__init__.py
index e35dbce2..bfae954e 100644
--- a/mediagoblin/celery_setup/__init__.py
+++ b/mediagoblin/init/celery/__init__.py
@@ -20,7 +20,7 @@ import sys
MANDATORY_CELERY_IMPORTS = ['mediagoblin.process_media']
-DEFAULT_SETTINGS_MODULE = 'mediagoblin.celery_setup.dummy_settings_module'
+DEFAULT_SETTINGS_MODULE = 'mediagoblin.init.celery.dummy_settings_module'
def setup_celery_from_config(app_config, global_config,
@@ -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/celery_setup/dummy_settings_module.py b/mediagoblin/init/celery/dummy_settings_module.py
index e69de29b..e69de29b 100644
--- a/mediagoblin/celery_setup/dummy_settings_module.py
+++ b/mediagoblin/init/celery/dummy_settings_module.py
diff --git a/mediagoblin/celery_setup/from_celery.py b/mediagoblin/init/celery/from_celery.py
index ed0a409e..c053591b 100644
--- a/mediagoblin/celery_setup/from_celery.py
+++ b/mediagoblin/init/celery/from_celery.py
@@ -17,7 +17,7 @@
import os
from mediagoblin import app, mg_globals
-from mediagoblin.celery_setup import setup_celery_from_config
+from mediagoblin.init.celery import setup_celery_from_config
OUR_MODULENAME = __name__
diff --git a/mediagoblin/celery_setup/from_tests.py b/mediagoblin/init/celery/from_tests.py
index 779ecd65..b2293e2c 100644
--- a/mediagoblin/celery_setup/from_tests.py
+++ b/mediagoblin/init/celery/from_tests.py
@@ -17,7 +17,7 @@
import os
from mediagoblin.tests.tools import TEST_APP_CONFIG
-from mediagoblin.celery_setup.from_celery import setup_self
+from mediagoblin.init.celery.from_celery import setup_self
OUR_MODULENAME = __name__
diff --git a/mediagoblin/config.py b/mediagoblin/init/config.py
index 2f93d32c..2f93d32c 100644
--- a/mediagoblin/config.py
+++ b/mediagoblin/init/config.py
diff --git a/mediagoblin/mg_globals.py b/mediagoblin/mg_globals.py
index 739f44ee..12a0e016 100644
--- a/mediagoblin/mg_globals.py
+++ b/mediagoblin/mg_globals.py
@@ -20,12 +20,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 9e0ceff7..125b24e0 100644
--- a/mediagoblin/process_media/__init__.py
+++ b/mediagoblin/process_media/__init__.py
@@ -21,7 +21,7 @@ from celery.task import task
from mediagoblin import mg_globals as mgg
-THUMB_SIZE = 200, 200
+THUMB_SIZE = 180, 180
MEDIUM_SIZE = 640, 640
@@ -54,7 +54,7 @@ def process_media_initial(media_id):
thumb_file = mgg.public_store.get_file(thumb_filepath, 'w')
with thumb_file:
- thumb.save(thumb_file, "JPEG")
+ thumb.save(thumb_file, "JPEG", quality=90)
"""
If the size of the original file exceeds the specified size of a `medium`
@@ -74,7 +74,7 @@ def process_media_initial(media_id):
medium_file = mgg.public_store.get_file(medium_filepath, 'w')
with medium_file:
- medium.save(medium_file, "JPEG")
+ medium.save(medium_file, "JPEG", quality=90)
medium_processed = True
# we have to re-read because unlike PIL, not everything reads
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index 31573820..31b8ebc2 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -1,6 +1,6 @@
body {
- background-color: #272727;
- color: #f7f7f7;
+ background-color: #1F1F1F;
+ color: #aaa;
font-family: sans-serif;
padding:none;
margin:0px;
@@ -18,24 +18,23 @@ form {
font-family: 'Carter One';
font-style: normal;
font-weight: normal;
- src: local('CarterOne'), url('http://themes.googleusercontent.com/font?kit=VjW2qt1pkqVtO22ObxgEBRsxEYwM7FgeyaSgU71cLG0') format('woff');
+ src: local('CarterOne'), url('http://themes.googleusercontent.com/font?kit=FWNn6ITYqL6or7ZTmBxRhq3fkYX5z1QtDUdIWoaaD_k') format('woff');
}
/* text styles */
-h1 {
+h1{
font-family: 'Carter One', arial, serif;
- margin-bottom: 20px;
- margin-top:40px;
+ margin-bottom: 15px;
+ margin-top:15px;
}
-p {
- font-family: sans-serif;
- font-size:16px;
+h2{
+ margin-top:20px;
}
a {
- color: #86D4B1;
+ color: #fff;
}
label {
@@ -52,15 +51,33 @@ label {
.mediagoblin_header {
width:100%;
height:36px;
- background-color:#393939;
+ background-color:#2F2F2F;
padding-top:14px;
margin-bottom:40px;
}
+.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;
+ height:30px;
+ background-color:#2F2F2F;
bottom:0px;
padding-top:8px;
position:absolute;
@@ -73,75 +90,12 @@ label {
padding-bottom:74px;
}
-ul.mediagoblin_messages {
- list-style:none inside;
- color:#393932;
- margin:2px;
- padding:2px;
-}
-
-ul.mediagoblin_messages li {
- background-color:#d4d4d4;
- border-style:solid;
- border-width:3px;
- border-color:#959595;
- margin:5px;
- padding:8px;
-}
-
-ul.mediagoblin_messages li.message_success {
- background-color: #88d486;
- border-color: #5bba59;
-}
-
-ul.mediagoblin_messages li.message_warning {
- background-color: #d4c686;
- border-color: #baa959;
-}
-
-ul.mediagoblin_messages li.message_error {
- background-color: #d48686;
- border-color: #ba5959;
-}
-
-ul.mediagoblin_messages li.message_info {
- background-color: #86b9d4;
- border-color: #5998ba;
-}
-
-ul.mediagoblin_messages li.message_debug {
- background-color: #aa86d4;
- border-color: #8659ba;
-}
-
-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;
-}
-
.mediagoblin_header_right {
float:right;
}
/* common website elements */
-.dotted_line {
- width:100%;
- height:0px;
- border-bottom: dotted 1px #5f5f5f;
- position:absolute;
- left:0px;
- margin-top:-20px;
-}
-
.button {
font-family:'Carter One', arial, serif;
height:32px;
@@ -164,6 +118,14 @@ a.mediagoblin_logo:hover {
padding-right:11px;
}
+.pagination{
+text-align:center;
+}
+
+.pagination_arrow{
+ margin:5px;
+}
+
/* forms */
.form_box {
@@ -172,7 +134,7 @@ a.mediagoblin_logo:hover {
background-repeat:repeat-x;
font-size:18px;
padding-bottom:30px;
- padding-top:1px;
+ padding-top:30px;
margin-left:auto;
margin-right:auto;
display:block;
@@ -202,6 +164,7 @@ a.mediagoblin_logo:hover {
.form_field_error {
background-color:#87453b;
+ color:#fff;
border:none;
font-size:16px;
padding:9px;
@@ -213,30 +176,89 @@ a.mediagoblin_logo:hover {
text-align:right;
}
-/* media pages */
+/* comments */
-.media_image{
- width:640px;
+.comment_author {
+ margin-bottom:40px;
+ padding-top:4px;
}
-.media_sidebar{
- width:280px;
+.comment_content p {
+ margin-bottom:4px;
}
/* media galleries */
-ul.media_thumbnail {
+.media_thumbnail {
padding:0px;
+ width:180px;
+ height:180px;
+ overflow:hidden;
+ float:left;
+ margin:0px 4px 10px 4px;
+ text-align:center;
}
-li.media_thumbnail {
- width:200px;
- height:200px;
- display:-moz-inline-stack;
- display:inline-block;
- vertical-align:top;
- margin:0px 10px 10px 0px;
+/* icons */
+
+img.media_icon{
+ margin:0 4px;
+ vertical-align:sub;
+}
+
+/* navigation */
+
+.navigation_button{
+ width:139px;
+ display:block;
+ float:left;
text-align:center;
- zoom:1;
-. *display:inline;
+ background-color:#393939;
+ text-decoration:none;
+ padding:12px 0pt;
+ font-family:'Carter One', arial, serif;
+ font-size:2em;
+ margin:0 0 20px
+}
+
+p.navigation_button{
+ color:#272727;
+}
+
+.navigation_left{
+ margin-right:2px;
+}
+
+/* messages */
+
+ul.mediagoblin_messages {
+ list-style:none inside;
+ color:#f7f7f7;
+}
+
+.mediagoblin_messages li {
+ margin:5px 0;
+ padding:8px;
+ text-align:center;
+}
+
+.message_success {
+ background-color: #378566;
+}
+
+.message_warning {
+ background-color: #87453b;
+}
+
+.message_error {
+ background-color: #87453b;
+}
+
+.message_info {
+ background-color: #378566;
+}
+
+.message_debug {
+ background-color: #f7f7f7;
+ color:#272727;
}
diff --git a/mediagoblin/static/css/contrib/960_12_col.css b/mediagoblin/static/css/contrib/960_12_col.css
deleted file mode 120000
index 15c360e4..00000000
--- a/mediagoblin/static/css/contrib/960_12_col.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../contrib/960_12_col.css \ No newline at end of file
diff --git a/mediagoblin/static/css/contrib/960_16_col.css b/mediagoblin/static/css/contrib/960_16_col.css
new file mode 120000
index 00000000..bc1a430c
--- /dev/null
+++ b/mediagoblin/static/css/contrib/960_16_col.css
@@ -0,0 +1 @@
+../../../contrib/960_16_col.css \ No newline at end of file
diff --git a/mediagoblin/static/css/contrib/reset.css b/mediagoblin/static/css/contrib/reset.css
new file mode 120000
index 00000000..87ae5592
--- /dev/null
+++ b/mediagoblin/static/css/contrib/reset.css
@@ -0,0 +1 @@
+../../../contrib/reset.css \ No newline at end of file
diff --git a/mediagoblin/static/css/contrib/text.css b/mediagoblin/static/css/contrib/text.css
new file mode 120000
index 00000000..d75ce48b
--- /dev/null
+++ b/mediagoblin/static/css/contrib/text.css
@@ -0,0 +1 @@
+../../../contrib/text.css \ No newline at end of file
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/icon_delete.png b/mediagoblin/static/images/icon_delete.png
new file mode 100644
index 00000000..9d76a5db
--- /dev/null
+++ b/mediagoblin/static/images/icon_delete.png
Binary files differ
diff --git a/mediagoblin/static/images/icon_edit.png b/mediagoblin/static/images/icon_edit.png
new file mode 100644
index 00000000..480c73ad
--- /dev/null
+++ b/mediagoblin/static/images/icon_edit.png
Binary files differ
diff --git a/mediagoblin/static/images/icon_feed.png b/mediagoblin/static/images/icon_feed.png
new file mode 100644
index 00000000..11e5b1e7
--- /dev/null
+++ b/mediagoblin/static/images/icon_feed.png
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/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 6139614e..1848f5e5 100644
--- a/mediagoblin/submit/views.py
+++ b/mediagoblin/submit/views.py
@@ -24,6 +24,7 @@ from mediagoblin.util import (
from mediagoblin.decorators import require_active_login
from mediagoblin.submit import forms as submit_forms, security
from mediagoblin.process_media import process_media_initial
+from mediagoblin.messages import add_message, SUCCESS
@require_active_login
@@ -85,14 +86,12 @@ def submit_start(request):
# queue it for processing
process_media_initial.delay(unicode(entry['_id']))
- return redirect(request, "mediagoblin.submit.success")
+ add_message(request, SUCCESS, 'Woohoo! Submitted!')
+
+ return redirect(request, "mediagoblin.user_pages.user_home",
+ user = request.user['username'])
return render_to_response(
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 f6ee7166..e25783ea 100644
--- a/mediagoblin/templates/mediagoblin/auth/login.html
+++ b/mediagoblin/templates/mediagoblin/auth/login.html
@@ -20,10 +20,9 @@
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
-
<form action="{{ request.urlgen('mediagoblin.auth.login') }}"
method="POST" enctype="multipart/form-data">
- <div class="grid_4 prefix_1 suffix_1 form_box">
+ <div class="grid_6 prefix_1 suffix_1 form_box">
<h1>Log in</h1>
{% if login_failed %}
<div class="form_field_error">Login failed!</div>
@@ -36,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.html b/mediagoblin/templates/mediagoblin/auth/register.html
index 7e18ca58..f77b3782 100644
--- a/mediagoblin/templates/mediagoblin/auth/register.html
+++ b/mediagoblin/templates/mediagoblin/auth/register.html
@@ -23,7 +23,7 @@
<form action="{{ request.urlgen('mediagoblin.auth.register') }}"
method="POST" enctype="multipart/form-data">
- <div class="grid_4 prefix_1 suffix_1 form_box">
+ <div class="grid_6 prefix_1 suffix_1 form_box">
<h1>Create an account!</h1>
{{ wtforms_util.render_divs(register_form) }}
<div class="form_submit_buttons">
diff --git a/mediagoblin/templates/mediagoblin/auth/verify_email.html b/mediagoblin/templates/mediagoblin/auth/verify_email.html
deleted file mode 100644
index b6e6d1f8..00000000
--- a/mediagoblin/templates/mediagoblin/auth/verify_email.html
+++ /dev/null
@@ -1,28 +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>
- {% if verification_successful %}
- Your email address has been verified!
- {% else %}
- The verification key or user id is incorrect
- {% endif %}
-</p>
-{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html
index fbb52803..40bb085e 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -19,7 +19,11 @@
<head>
<title>{% block title %}GNU MediaGoblin{% endblock title %}</title>
<link rel="stylesheet" type="text/css"
- href="{{ request.staticdirect('/css/contrib/960_12_col.css') }}"/>
+ href="{{ request.staticdirect('/css/contrib/reset.css') }}"/>
+ <link rel="stylesheet" type="text/css"
+ href="{{ request.staticdirect('/css/contrib/text.css') }}"/>
+ <link rel="stylesheet" type="text/css"
+ href="{{ request.staticdirect('/css/contrib/960_16_col.css') }}"/>
<link rel="stylesheet" type="text/css"
href="{{ request.staticdirect('/css/base.css') }}"/>
{% block mediagoblin_head %}
@@ -31,11 +35,15 @@
<div class="mediagoblin_body">
{% block mediagoblin_header %}
<div class="mediagoblin_header">
- <div class="container_12">
- <div class="grid_12">
+ <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 %}
+ <a class="mediagoblin_logo" href="{{ request.urlgen('index') }}"><img src="{{ request.staticdirect('/images/logo.png') }}" alt="Mediagoblin logo" /></a>
+ {% endblock %}
+ {% if request.user %}
+ <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 %}
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
@@ -51,8 +59,8 @@
</div>
</div>
{% endblock %}
- <div class="container_12 mediagoblin_content">
- <div class="grid_12">
+ <div class="container_16 mediagoblin_content">
+ <div class="grid_16">
{% include "mediagoblin/utils/messages.html" %}
{% block mediagoblin_content %}
{% endblock mediagoblin_content %}
@@ -60,8 +68,8 @@
</div>
{% block mediagoblin_footer %}
<div class="mediagoblin_footer">
- <div class="container_12">
- <div class="grid_12">
+ <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>
diff --git a/mediagoblin/templates/mediagoblin/edit/edit.html b/mediagoblin/templates/mediagoblin/edit/edit.html
index 8ee09bd5..d19034cb 100644
--- a/mediagoblin/templates/mediagoblin/edit/edit.html
+++ b/mediagoblin/templates/mediagoblin/edit/edit.html
@@ -25,7 +25,7 @@
user= media.uploader().username,
media= media._id) }}"
method="POST" enctype="multipart/form-data">
- <div class="grid_6 prefix_1 suffix_1 edit_box form_box">
+ <div class="grid_8 prefix_1 suffix_1 edit_box form_box">
<h1>Editing {{ media.title }}</h1>
<div style="text-align: center;" >
<img src="{{ request.app.public_store.file_url(
diff --git a/mediagoblin/templates/mediagoblin/edit/edit_profile.html b/mediagoblin/templates/mediagoblin/edit/edit_profile.html
index 7efd0ee3..a11b86d7 100644
--- a/mediagoblin/templates/mediagoblin/edit/edit_profile.html
+++ b/mediagoblin/templates/mediagoblin/edit/edit_profile.html
@@ -21,10 +21,10 @@
{% block mediagoblin_content %}
- <form action="{{ request.urlgen('mediagoblin.edit.profile',
- user=user.username) }}"
+ <form action="{{ request.urlgen('mediagoblin.edit.profile') }}?username={{
+ user['username'] }}"
method="POST" enctype="multipart/form-data">
- <div class="grid_6 prefix_1 suffix_1 edit_box form_box">
+ <div class="grid_8 prefix_1 suffix_1 edit_box form_box">
<h1>Editing {{ user['username'] }}'s profile</h1>
{{ wtforms_util.render_divs(form) }}
<div class="form_submit_buttons">
diff --git a/mediagoblin/templates/mediagoblin/media_details.html b/mediagoblin/templates/mediagoblin/media_details.html
deleted file mode 100644
index 0e907616..00000000
--- a/mediagoblin/templates/mediagoblin/media_details.html
+++ /dev/null
@@ -1,37 +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 %}
- {# temporarily, an "image gallery" that isn't one really ;) #}
- {% if media %}
- <div class="grid_8 alpha media_image">
- <img src="{{ request.app.public_store.file_url(
- media.media_files.main) }}" />
- <h1>Media details for {{media.title}}</h1>
- <p>
- <br/>Uploaded: {{ media.created}}
- <br/>Description: {{media.description}}
- </p>
- </div>
- <div class="grid_4 omega sidebar">
- <p>Uploaded: {{ media.created}}</p>
- </div>
- {% else %}
- <p>Sorry, no such media found.<p/>
- {% endif %}
-{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/root.html b/mediagoblin/templates/mediagoblin/root.html
index e29abd51..bae033c4 100644
--- a/mediagoblin/templates/mediagoblin/root.html
+++ b/mediagoblin/templates/mediagoblin/root.html
@@ -23,17 +23,18 @@
{% if request.user %}
<p>
<a href="{{ request.urlgen('mediagoblin.submit.start') }}">Submit an item</a>
- <a href="{{ request.urlgen('mediagoblin.edit.profile') }}">Edit profile</a>
</p>
{% else %}
<p>
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/start.html b/mediagoblin/templates/mediagoblin/submit/start.html
index f34bf2af..50c86afe 100644
--- a/mediagoblin/templates/mediagoblin/submit/start.html
+++ b/mediagoblin/templates/mediagoblin/submit/start.html
@@ -20,17 +20,16 @@
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
-
<form action="{{ request.urlgen('mediagoblin.submit.start') }}"
method="POST" enctype="multipart/form-data">
- <div class="grid_6 prefix_1 suffix_1 form_box">
+ <div class="grid_8 prefix_1 suffix_1 form_box">
<h1>Submit yer media</h1>
+ {{ wtforms_util.render_field_div(submit_form.file) }}
{{ wtforms_util.render_field_div(submit_form.title) }}
{{ wtforms_util.render_textarea_div(submit_form.description) }}
- {{ wtforms_util.render_field_div(submit_form.file) }}
<div class="form_submit_buttons">
<input type="submit" value="Submit" class="button" />
</div>
</div>
</form>
-{% endblock %}
+{% endblock %}
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/gallery.html b/mediagoblin/templates/mediagoblin/user_pages/gallery.html
index 28290cfd..a434ff15 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/gallery.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/gallery.html
@@ -31,7 +31,11 @@
'mediagoblin.user_pages.user_home',
user=user.username) }}">{{ user.username }}</a>'s media</h1>
- {% include "mediagoblin/utils/object_gallery.html" %}
+ </div>
+ <div class="container_16 media_gallery">
+ {% include "mediagoblin/utils/object_gallery.html" %}
+ </div>
+ <div class="grid_16">
<a href={{ request.urlgen(
'mediagoblin.user_pages.atom_feed',
diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html
index 56d79662..dc0b6210 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/media.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/media.html
@@ -18,86 +18,106 @@
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+{% from "mediagoblin/utils/pagination.html" import render_pagination %}
{% block mediagoblin_content %}
- {# temporarily, an "image gallery" that isn't one really ;) #}
{% if media %}
- <div class="grid_8 alpha media_image">
- <h1>
- {{media.title}}
- </h1>
+ <div class="grid_11 alpha">
<img class="media_image" src="{{ request.app.public_store.file_url(
media.get_display_media(media.media_files)) }}" />
+
+ <h2>
+ {{media.title}}
+ </h2>
+
+ {% autoescape False %}
+ <p>{{ media.description_html }}</p>
+ {% endautoescape %}
+
<p>
- Uploaded on
+ &mdash;&nbsp;uploaded on
{{ "%4d-%02d-%02d"|format(media.created.year,
media.created.month, media.created.day) }}
by
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
user= media.uploader().username) }}">
{{- media.uploader().username }}</a>
- </p>
-
- {% autoescape False %}
- <p>{{ media.description_html }}</p>
- {% endautoescape %}
+ </p>
+ <br />
- {% if media['uploader'] == request.user['_id'] %}
- <p><a href="{{ request.urlgen('mediagoblin.edit.edit_media',
- user= media.uploader().username,
- media= media._id) }}">Edit</a></p>
- {% endif %}
+ <h3>Comments</h3>
{% if request.user %}
<form action="{{ request.urlgen('mediagoblin.user_pages.media_post_comment',
user= media.uploader().username,
media=media._id) }}" method="POST">
- <h3>Post a comment!</h3>
- {{ 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="Submit" class="button" />
+ <input type="submit" value="Post comment!" class="button" />
</div>
</form>
{% endif %}
- {#
- {{ wtforms_util.render_textarea_div(submit_form.description) }}
- {{ wtforms_util.render_field_div(submit_form.file) }}
- #}
-
{% if comments %}
- <h3>Comments</h3>
{% for comment in comments %}
- <div class="comment_wrapper" id="comment-{{ comment['_id'] }}">
- <div class="comment_author">By:
- <a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
- user = comment['author']['username']) }}">
- {{ comment['author']['username'] }}
- </a>
- </div>
- <div class="comment_datetime">
- <a href="#comment-{{ comment['_id'] }}">
- {{ "%4d-%02d-%02d %02d:%02d"|format(comment.created.year,
- comment.created.month,
- comment.created.day,
- comment.created.hour,
- comment.created.minute) }}
- </a>
- </div>
+ {% set comment_author = comment.author() %}
+ {% if pagination.active_id == comment._id %}
+ <div class="comment_wrapper comment_active" id="comment-{{ comment['_id'] }}">
+ <a name="comment" id="comment"></a>
+ {% else %}
+ <div class="comment_wrapper" id="comment-{{ comment['_id'] }}">
+ {% endif %}
<div class="comment_content">
{% autoescape False %}
{{ comment.content_html }}
{% endautoescape %}
- </div>
- </div>
+ </div>
+ <div class="comment_author">&mdash;
+ <a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
+ user = comment_author['username']) }}">
+ {{ comment_author['username'] }}</a> at
+ <!--</div>
+ <div class="comment_datetime">-->
+ <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,
+ comment.created.hour,
+ comment.created.minute) }}
+ </a>
+ </div>
+ </div>
{% endfor %}
- {% include "mediagoblin/utils/pagination.html" %}
+
+ {{ render_pagination(request, pagination,
+ request.urlgen('mediagoblin.user_pages.media_home',
+ user = media.uploader().username,
+ media = media._id)) }}
</div>
{% endif %}
-
- <div class="grid_4 omega media_sidebar">
- <p>This is a sidebar! Yay!</p>
+ <div class="grid_5 omega">
+ {% include "mediagoblin/utils/prev_next.html" %}
+ <h3>Sidebar content here!</h3>
+ <p>
+ {% if media['uploader'] == request.user['_id'] or
+ request.user['is_admin'] %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.edit.edit_media',
+ user= media.uploader().username,
+ media= media._id) }}"
+ ><img src="{{ request.staticdirect('/images/icon_edit.png') }}"
+ class="media_icon" />edit</a>
+ </p>
+ <p>
+ <img src="{{ request.staticdirect('/images/icon_delete.png') }}"
+ class="media_icon" />delete
+ </p>
+ {% endif %}
+ </p>
</div>
{% else %}
- <p>Sorry, no such media found.<p/>
+ <p>Sorry, no such media found.<p/>
{% endif %}
{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html
index 99e46a72..9d99ac53 100644
--- a/mediagoblin/templates/mediagoblin/user_pages/user.html
+++ b/mediagoblin/templates/mediagoblin/user_pages/user.html
@@ -20,27 +20,31 @@
{% block mediagoblin_head %}
<link rel="alternate" type="application/atom+xml"
href="{{ request.urlgen(
- 'mediagoblin.user_pages.atom_feed',
+ 'mediagoblin.user_pages.atom_feed',
user=user.username) }}">
{% endblock mediagoblin_head %}
{% block mediagoblin_content -%}
{% if user %}
- <h1>{{ user.username }}'s profile</h1>
-
- {% include "mediagoblin/utils/profile.html" %}
-
- {% include "mediagoblin/utils/object_gallery.html" %}
-
- <p><a href="{{ request.urlgen('mediagoblin.user_pages.user_gallery',
- user= request.user['username']) }}">View all of {{ user.username }}'s media</a></p>
-
-
- <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/>
+ <h1>{{ user.username }}'s profile</h1>
+ <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>
+ <div class="grid_10 omega">
+ {% set pagination_base_url = user_gallery_url %}
+ {% include "mediagoblin/utils/object_gallery.html" %}
+ <div class="clear"></div>
+ <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>
+ {% else %}
+ {# This *should* not occur as the view makes sure we pass in a user. #}
+ <p>Sorry, no such user found.<p/>
+ </div>
{% endif %}
-{% endblock %}
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/utils/object_gallery.html b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
index 8c88c174..2c7a7129 100644
--- a/mediagoblin/templates/mediagoblin/utils/object_gallery.html
+++ b/mediagoblin/templates/mediagoblin/utils/object_gallery.html
@@ -16,20 +16,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
-{% block object_gallery_content -%}
- <div>
- {% if media_entries %}
- <ul class="media_thumbnail">
- {% for entry in media_entries %}
- <li class="media_thumbnail">
- <a href="{{ entry.url_for_self(request.urlgen) }}">
- <img src="{{ request.app.public_store.file_url(
- entry['media_files']['thumb']) }}" /></a>
- </li>
- {% endfor %}
- </ul>
- {% include "mediagoblin/utils/pagination.html" %}
- {% endif %}
+{% from "mediagoblin/utils/pagination.html" import render_pagination %}
- </div>
-{% endblock %}
+{% block object_gallery_content -%}
+ {% if media_entries %}
+ {% for entry in media_entries %}
+ <div class="media_thumbnail">
+ <a href="{{ entry.url_for_self(request.urlgen) }}">
+ <img src="{{ request.app.public_store.file_url(
+ entry['media_files']['thumb']) }}" /></a>
+ </div>
+ {% endfor %}
+ {% if pagination_base_url %}
+ {# different url, so set that and don't keep the get params #}
+ {{ render_pagination(request, pagination, pagination_base_url, False) }}
+ {% else %}
+ {{ render_pagination(request, pagination) }}
+ {% endif %}
+ {% endif %}
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/utils/pagination.html b/mediagoblin/templates/mediagoblin/utils/pagination.html
index 2be0b92e..23d49463 100644
--- a/mediagoblin/templates/mediagoblin/utils/pagination.html
+++ b/mediagoblin/templates/mediagoblin/utils/pagination.html
@@ -15,30 +15,49 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
-{# only display if {{pagination}} is defined #}
+{% macro render_pagination(request, pagination,
+ base_url=None, preserve_get_params=True) %}
+ {# only display if {{pagination}} is defined #}
+ {% if pagination and pagination.pages > 1 %}
+ {% if not base_url %}
+ {% set base_url = request.path_info %}
+ {% endif %}
-{% if pagination %}
- <div class="pagination">
+ {% if preserve_get_params %}
+ {% set get_params = request.GET %}
+ {% else %}
+ {% set get_params = {} %}
+ {% endif %}
- {% if pagination.has_prev %}
- <a href="{{ pagination.get_page_url(request, pagination.page-1) }}">&laquo; Prev</>
- {% endif %}
-
- {%- for page in pagination.iter_pages() %}
- {% if page %}
- {% if page != pagination.page %}
- <a href="{{ pagination.get_page_url(request, page) }}">{{ page }}</a>
+ <div class="pagination">
+ <p>
+ {% if pagination.has_prev %}
+ <a href="{{ pagination.get_page_url_explicit(
+ base_url, get_params,
+ 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 %}
+ <a href="{{ pagination.get_page_url_explicit(
+ base_url, get_params,
+ page) }}">{{ page }}</a>
+ {% else %}
+ {{ page }}
+ {% endif %}
{% else %}
- <strong>{{ page }}</strong>
+ <span class="ellipsis">…</span>
{% endif %}
- {% else %}
- <span class="ellipsis">…</span>
- {% endif %}
- {%- endfor %}
-
- {% if pagination.has_next %}
- <a href="{{ pagination.get_page_url(request, pagination.page + 1) }}">Next &raquo;</a>
- {% endif %}
- </div>
-{% endif %}
-
+ {%- endfor %}
+ </p>
+ </div>
+ {% endif %}
+{% endmacro %}
diff --git a/mediagoblin/templates/mediagoblin/utils/prev_next.html b/mediagoblin/templates/mediagoblin/utils/prev_next.html
new file mode 100644
index 00000000..7cf8d2a4
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/utils/prev_next.html
@@ -0,0 +1,46 @@
+{#
+# 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/>.
+#}
+
+{# Provide navigation links to neighboring media entries, if possible #}
+{% set prev_entry_url = media.url_to_prev(request.urlgen) %}
+{% set next_entry_url = media.url_to_next(request.urlgen) %}
+
+<div>
+ {# 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 }}">
+ <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 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 }}">
+ <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">
+ <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 b3f5f0f8..63024b77 100644
--- a/mediagoblin/templates/mediagoblin/utils/profile.html
+++ b/mediagoblin/templates/mediagoblin/utils/profile.html
@@ -17,19 +17,14 @@
#}
{% block profile_content -%}
- <div>
- <ul>
- {% if user.url %}
- <li>
- <a href="{{ user.url }}">homepage</a>
- </li>
- {% endif %}
-
- {% if user.bio %}
- <li>
- {{ user.bio }}
- </li>
- {% endif %}
- </ul>
- </div>
-{% endblock %}
+ {% if user.bio %}
+ {% autoescape False %}
+ <p>{{ user.bio_html }}</p>
+ {% endautoescape %}
+ {% endif %}
+ {% 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 3a13cbb1..ad9dd35b 100644
--- a/mediagoblin/tests/test_auth.py
+++ b/mediagoblin/tests/test_auth.py
@@ -189,7 +189,7 @@ def test_register_views(test_app):
"/auth/verify_email/?userid=%s&token=total_bs" % unicode(
new_user['_id']))
context = util.TEMPLATE_TEST_CONTEXT[
- 'mediagoblin/auth/verify_email.html']
+ 'mediagoblin/user_pages/user.html']
assert context['verification_successful'] == False
new_user = mg_globals.database.User.find_one(
{'username': 'happygirl'})
@@ -201,7 +201,7 @@ def test_register_views(test_app):
util.clear_test_template_context()
test_app.get("%s?%s" % (path, get_params))
context = util.TEMPLATE_TEST_CONTEXT[
- 'mediagoblin/auth/verify_email.html']
+ 'mediagoblin/user_pages/user.html']
assert context['verification_successful'] == True
new_user = mg_globals.database.User.find_one(
{'username': 'happygirl'})
diff --git a/mediagoblin/tests/test_celery_setup.py b/mediagoblin/tests/test_celery_setup.py
index 8bf97ae4..b80cab49 100644
--- a/mediagoblin/tests/test_celery_setup.py
+++ b/mediagoblin/tests/test_celery_setup.py
@@ -16,8 +16,8 @@
import pkg_resources
-from mediagoblin import celery_setup
-from mediagoblin.config import read_mediagoblin_config
+from mediagoblin.init import celery as celery_setup
+from mediagoblin.init.config import read_mediagoblin_config
TEST_CELERY_CONF_NOSPECIALDB = pkg_resources.resource_filename(
diff --git a/mediagoblin/tests/test_config.py b/mediagoblin/tests/test_config.py
index 244f05e5..f9f12072 100644
--- a/mediagoblin/tests/test_config.py
+++ b/mediagoblin/tests/test_config.py
@@ -16,7 +16,7 @@
import pkg_resources
-from mediagoblin import config
+from mediagoblin.init import config
CARROT_CONF_GOOD = pkg_resources.resource_filename(
diff --git a/mediagoblin/tests/test_mgoblin_app.ini b/mediagoblin/tests/test_mgoblin_app.ini
index e022d47b..fd0f87a4 100644
--- a/mediagoblin/tests/test_mgoblin_app.ini
+++ b/mediagoblin/tests/test_mgoblin_app.ini
@@ -8,7 +8,7 @@ email_debug_mode = true
db_name = __mediagoblin_tests__
# Celery shouldn't be set up by the application as it's setup via
-# mediagoblin.celery_setup.from_celery
+# mediagoblin.init.celery.from_celery
celery_setup_elsewhere = true
[celery]
diff --git a/mediagoblin/tests/test_migrations.py b/mediagoblin/tests/test_migrations.py
new file mode 100644
index 00000000..127b90e1
--- /dev/null
+++ b/mediagoblin/tests/test_migrations.py
@@ -0,0 +1,402 @@
+# 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 should
+ """
+ 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 64f773f0..4b61f259 100644
--- a/mediagoblin/tests/tools.py
+++ b/mediagoblin/tests/tools.py
@@ -22,7 +22,7 @@ from paste.deploy import loadapp
from webtest import TestApp
from mediagoblin import util
-from mediagoblin.config import read_mediagoblin_config
+from mediagoblin.init.config import read_mediagoblin_config
from mediagoblin.decorators import _make_safe
from mediagoblin.db.open import setup_connection_and_db_from_config
@@ -42,8 +42,8 @@ USER_DEV_DIRECTORIES_TO_SETUP = [
BAD_CELERY_MESSAGE = """\
Sorry, you *absolutely* must run nosetests with the
-mediagoblin.celery_setup.from_tests module. Like so:
-$ CELERY_CONFIG_MODULE=mediagoblin.celery_setup.from_tests ./bin/nosetests"""
+mediagoblin.init.celery.from_tests module. Like so:
+$ CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_tests ./bin/nosetests"""
class BadCeleryEnviron(Exception): pass
@@ -51,7 +51,7 @@ class BadCeleryEnviron(Exception): pass
def suicide_if_bad_celery_environ():
if not os.environ.get('CELERY_CONFIG_MODULE') == \
- 'mediagoblin.celery_setup.from_tests':
+ 'mediagoblin.init.celery.from_tests':
raise BadCeleryEnviron(BAD_CELERY_MESSAGE)
@@ -59,7 +59,7 @@ def get_test_app(dump_old_app=True):
suicide_if_bad_celery_environ()
# Leave this imported as it sets up celery.
- from mediagoblin.celery_setup import from_tests
+ from mediagoblin.init.celery import from_tests
global MGOBLIN_APP
@@ -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/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 399d2020..a3172ebd 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -48,10 +48,15 @@ def user_home(request, page):
if media_entries == None:
return exc.HTTPNotFound()
+ user_gallery_url = request.urlgen(
+ 'mediagoblin.user_pages.user_gallery',
+ user=user['username'])
+
return render_to_response(
request,
'mediagoblin/user_pages/user.html',
{'user': user,
+ 'user_gallery_url': user_gallery_url,
'media_entries': media_entries,
'pagination': pagination})
@@ -82,17 +87,25 @@ def user_gallery(request, page):
'media_entries': media_entries,
'pagination': pagination})
+MEDIA_COMMENTS_PER_PAGE = 50
@get_user_media_entry
@uses_pagination
-def media_home(request, media, **kwargs):
+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)
- comment_form = user_forms.MediaCommentForm(request.POST)
+ comments = pagination()
- (comments, pagination) = media.get_comments(kwargs.get('page'))
+ comment_form = user_forms.MediaCommentForm(request.POST)
return render_to_response(
request,
@@ -111,7 +124,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 a5425663..1892378c 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,7 +23,7 @@ import smtplib
import sys
import re
import urllib
-from math import ceil
+from math import ceil, floor
import copy
from babel.localedata import exists
@@ -35,6 +37,8 @@ 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
@@ -66,22 +70,6 @@ def clear_test_buckets():
clear_test_template_context()
-def get_jinja_loader(user_template_path=None):
- """
- Set up the Jinja template loaders, possibly allowing for user
- overridden templates.
-
- (In the future we may have another system for providing theming;
- for now this is good enough.)
- """
- if user_template_path:
- return jinja2.ChoiceLoader(
- [jinja2.FileSystemLoader(user_template_path),
- jinja2.PackageLoader('mediagoblin', 'templates')])
- else:
- return jinja2.PackageLoader('mediagoblin', 'templates')
-
-
SETUP_JINJA_ENVS = {}
@@ -151,7 +139,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):
@@ -270,9 +267,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()
@@ -285,7 +282,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']
@@ -436,7 +433,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
@@ -444,11 +442,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):