diff options
Diffstat (limited to 'mediagoblin')
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 Binary files differdeleted file mode 100644 index 4f4f3e9c..00000000 --- a/mediagoblin/static/images/icon.png +++ /dev/null diff --git a/mediagoblin/static/images/icon_delete.png b/mediagoblin/static/images/icon_delete.png Binary files differnew file mode 100644 index 00000000..9d76a5db --- /dev/null +++ b/mediagoblin/static/images/icon_delete.png diff --git a/mediagoblin/static/images/icon_edit.png b/mediagoblin/static/images/icon_edit.png Binary files differnew file mode 100644 index 00000000..480c73ad --- /dev/null +++ b/mediagoblin/static/images/icon_edit.png diff --git a/mediagoblin/static/images/icon_feed.png b/mediagoblin/static/images/icon_feed.png Binary files differnew file mode 100644 index 00000000..11e5b1e7 --- /dev/null +++ b/mediagoblin/static/images/icon_feed.png diff --git a/mediagoblin/static/images/logo.png b/mediagoblin/static/images/logo.png Binary files differnew file mode 100644 index 00000000..cf28a6d4 --- /dev/null +++ b/mediagoblin/static/images/logo.png diff --git a/mediagoblin/static/images/navigation_end.png b/mediagoblin/static/images/navigation_end.png Binary files differnew file mode 100644 index 00000000..b2f27296 --- /dev/null +++ b/mediagoblin/static/images/navigation_end.png diff --git a/mediagoblin/static/images/navigation_left.png b/mediagoblin/static/images/navigation_left.png Binary files differnew file mode 100644 index 00000000..d1645120 --- /dev/null +++ b/mediagoblin/static/images/navigation_left.png diff --git a/mediagoblin/static/images/navigation_right.png b/mediagoblin/static/images/navigation_right.png Binary files differnew file mode 100644 index 00000000..d4caa7b8 --- /dev/null +++ b/mediagoblin/static/images/navigation_right.png diff --git a/mediagoblin/static/images/pagination_left.png b/mediagoblin/static/images/pagination_left.png Binary files differnew file mode 100644 index 00000000..56a26596 --- /dev/null +++ b/mediagoblin/static/images/pagination_left.png diff --git a/mediagoblin/static/images/pagination_right.png b/mediagoblin/static/images/pagination_right.png Binary files differnew file mode 100644 index 00000000..84f8abba --- /dev/null +++ b/mediagoblin/static/images/pagination_right.png 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 + — 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">— + <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) }}">« 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 »</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 Binary files differnew file mode 100755 index 00000000..775da664 --- /dev/null +++ b/mediagoblin/tests/test_submission/evil diff --git a/mediagoblin/tests/test_submission/evil.jpg b/mediagoblin/tests/test_submission/evil.jpg Binary files differnew file mode 100755 index 00000000..775da664 --- /dev/null +++ b/mediagoblin/tests/test_submission/evil.jpg diff --git a/mediagoblin/tests/test_submission/evil.png b/mediagoblin/tests/test_submission/evil.png Binary files differnew file mode 100755 index 00000000..775da664 --- /dev/null +++ b/mediagoblin/tests/test_submission/evil.png diff --git a/mediagoblin/tests/test_submission/good.jpg b/mediagoblin/tests/test_submission/good.jpg Binary files differnew file mode 100644 index 00000000..936458e9 --- /dev/null +++ b/mediagoblin/tests/test_submission/good.jpg diff --git a/mediagoblin/tests/test_submission/good.png b/mediagoblin/tests/test_submission/good.png Binary files differnew file mode 100644 index 00000000..c1eadf9c --- /dev/null +++ b/mediagoblin/tests/test_submission/good.png 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): |