Merge branch 'master' into test_submission_views_365

This commit is contained in:
Chris Moylan 2011-06-28 23:05:03 -05:00
commit 4ce57ac85d
20 changed files with 515 additions and 78 deletions

View File

@ -40,10 +40,6 @@ def setup_celery_from_config(app_config, global_config,
- set_environ: if set, this will CELERY_CONFIG_MODULE to the
settings_module
"""
if app_config.get('celery_setup_elsewhere') == True:
# Don't setup celery based on our config file.
return
if global_config.has_key('celery'):
celery_conf = global_config['celery']
else:

View File

@ -23,7 +23,8 @@ from mediagoblin.celery_setup import setup_celery_from_config
OUR_MODULENAME = __name__
def setup_self(check_environ_for_conf=True, module_name=OUR_MODULENAME):
def setup_self(check_environ_for_conf=True, module_name=OUR_MODULENAME,
default_conf_file='mediagoblin.ini'):
"""
Transform this module into a celery config module by reading the
mediagoblin config file. Set the environment variable
@ -36,9 +37,9 @@ def setup_self(check_environ_for_conf=True, module_name=OUR_MODULENAME):
"""
if check_environ_for_conf:
mgoblin_conf_file = os.path.abspath(
os.environ.get('MEDIAGOBLIN_CONFIG', 'mediagoblin.ini'))
os.environ.get('MEDIAGOBLIN_CONFIG', default_conf_file))
else:
mgoblin_conf_file = 'mediagoblin.ini'
mgoblin_conf_file = default_conf_file
if not os.path.exists(mgoblin_conf_file):
raise IOError(
@ -48,6 +49,7 @@ def setup_self(check_environ_for_conf=True, module_name=OUR_MODULENAME):
# this is the module that gets set up.
os.environ['CELERY_CONFIG_MODULE'] = module_name
app.MediaGoblinApp(mgoblin_conf_file, setup_celery=False)
setup_celery_from_config(
mg_globals.app_config, mg_globals.global_config,
settings_module=module_name,

View File

@ -16,11 +16,15 @@
import os
from mediagoblin.tests.tools import TEST_APP_CONFIG
from mediagoblin.celery_setup.from_celery import setup_self
OUR_MODULENAME = __name__
CELERY_SETUP = False
if os.environ.get('CELERY_CONFIG_MODULE') == OUR_MODULENAME:
setup_self(check_environ_for_conf=False, module_name=OUR_MODULENAME)
setup_self(check_environ_for_conf=False, module_name=OUR_MODULENAME,
default_conf_file=TEST_APP_CONFIG)
CELERY_SETUP = True

118
mediagoblin/db/indexes.py Normal file
View File

@ -0,0 +1,118 @@
# 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/>.
"""
Indexes for the local database.
To add new indexes
------------------
Indexes are recorded in the following format:
ACTIVE_INDEXES = {
'collection_name': {
'identifier': { # key identifier used for possibly deprecating later
'index': [index_foo_goes_here]}}
... and anything else being parameters to the create_index function
(including unique=True, etc)
Current indexes must be registered in ACTIVE_INDEXES... deprecated
indexes should be marked in DEPRECATED_INDEXES.
Remember, ordering of compound indexes MATTERS. Read below for more.
REQUIRED READING:
- http://kylebanker.com/blog/2010/09/21/the-joy-of-mongodb-indexes/
- http://www.mongodb.org/display/DOCS/Indexes
- http://www.mongodb.org/display/DOCS/Indexing+Advice+and+FAQ
To remove deprecated indexes
----------------------------
Removing deprecated indexes is easier, just do:
INACTIVE_INDEXES = {
'collection_name': [
'deprecated_index_identifier1', 'deprecated_index_identifier2']}
... etc.
If an index has been deprecated that identifier should NEVER BE USED
AGAIN. Eg, if you previously had 'awesomepants_unique', you shouldn't
use 'awesomepants_unique' again, you should create a totally new name
or at worst use 'awesomepants_unique2'.
"""
from pymongo import ASCENDING, DESCENDING
################
# Active indexes
################
ACTIVE_INDEXES = {}
# MediaEntry indexes
# ------------------
MEDIAENTRY_INDEXES = {
'uploader_slug_unique': {
# Matching an object to an uploader + slug.
# MediaEntries are unique on these two combined, eg:
# /u/${myuser}/m/${myslugname}/
'index': [('uploader', ASCENDING),
('slug', ASCENDING)],
'unique': True},
'created': {
# A global index for all media entries created, in descending
# order. This is used for the site's frontpage.
'index': [('created', DESCENDING)]},
'uploader_created': {
# Indexing on uploaders and when media entries are created.
# Used for showing a user gallery, etc.
'index': [('uploader', ASCENDING),
('created', DESCENDING)]}}
ACTIVE_INDEXES['media_entries'] = MEDIAENTRY_INDEXES
# User indexes
# ------------
USER_INDEXES = {
'username_unique': {
# Index usernames, and make sure they're unique.
# ... I guess we might need to adjust this once we're federated :)
'index': 'username',
'unique': True},
'created': {
# All most recently created users
'index': 'created'}}
ACTIVE_INDEXES['users'] = USER_INDEXES
####################
# Deprecated indexes
####################
DEPRECATED_INDEXES = {}

View File

@ -108,11 +108,6 @@ class MediaEntry(Document):
migration_handler = migrations.MediaEntryMigration
indexes = [
# Referene uniqueness of slugs by uploader
{'fields': ['uploader', 'slug'],
'unique': True}]
def main_mediafile(self):
pass

View File

@ -14,8 +14,88 @@
# 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/>.
"""
Utilities for database operations.
Some note on migration and indexing tools:
We store information about what the state of the database is in the
'mediagoblin' document of the 'app_metadata' collection. Keys in that
document relevant to here:
- 'migration_number': The integer representing the current state of
the migrations
"""
import copy
# Imports that other modules might use
from pymongo import DESCENDING
from pymongo.errors import InvalidId
from mongokit import ObjectId
from mediagoblin.db.indexes import ACTIVE_INDEXES, DEPRECATED_INDEXES
def add_new_indexes(database, active_indexes=ACTIVE_INDEXES):
"""
Add any new indexes to the database.
Args:
- database: pymongo or mongokit database instance.
- active_indexes: indexes to possibly add in the pattern of:
{'collection_name': {
'identifier': {
'index': [index_foo_goes_here],
'unique': True}}
where 'index' is the index to add and all other options are
arguments for collection.create_index.
Returns:
A list of indexes added in form ('collection', 'index_name')
"""
indexes_added = []
for collection_name, indexes in active_indexes.iteritems():
collection = database[collection_name]
collection_indexes = collection.index_information().keys()
for index_name, index_data in indexes.iteritems():
if not index_name in collection_indexes:
# Get a copy actually so we don't modify the actual
# structure
index_data = copy.copy(index_data)
index = index_data.pop('index')
collection.create_index(
index, name=index_name, **index_data)
indexes_added.append((collection_name, index_name))
return indexes_added
def remove_deprecated_indexes(database, deprecated_indexes=DEPRECATED_INDEXES):
"""
Remove any deprecated indexes from the database.
Args:
- database: pymongo or mongokit database instance.
- deprecated_indexes: the indexes to deprecate in the pattern of:
{'collection': ['index_identifier1', 'index_identifier2']}
Returns:
A list of indexes removed in form ('collection', 'index_name')
"""
indexes_removed = []
for collection_name, index_names in deprecated_indexes.iteritems():
collection = database[collection_name]
collection_indexes = collection.index_information().keys()
for index_name in index_names:
if index_name in collection_indexes:
collection.drop_index(index_name)
indexes_removed.append((collection_name, index_name))
return indexes_removed

View File

@ -16,6 +16,7 @@
from mediagoblin.db import migrations
from mediagoblin.db import util as db_util
from mediagoblin.gmg_commands import util as commands_util
@ -27,8 +28,17 @@ def migrate_parser_setup(subparser):
def migrate(args):
mgoblin_app = commands_util.setup_app(args)
print "Applying migrations..."
# Clear old indexes
print "== Clearing old indexes... =="
removed_indexes = db_util.remove_deprecated_indexes(mgoblin_app.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)
@ -38,4 +48,10 @@ def migrate(args):
migration = model.migration_handler(model)
migration.migrate_all(collection=model.collection)
print "... done."
# Add new indexes
print "== Adding new indexes... =="
new_indexes = db_util.add_new_indexes(mgoblin_app.db)
for collection, index_name in new_indexes:
print "Added index '%s' to collection '%s'" % (
index_name, collection)

34
mediagoblin/messages.py Normal file
View File

@ -0,0 +1,34 @@
# 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/>.
DEBUG = 'debug'
INFO = 'info'
SUCCESS = 'success'
WARNING = 'warning'
ERROR = 'error'
def add_message(request, level, text):
messages = request.session.setdefault('messages', [])
messages.append({'level': level, 'text': text})
request.session.save()
def fetch_messages(request, clear_from_session=True):
messages = request.session.get('messages')
if messages and clear_from_session:
# Save that we removed the messages from the session
request.session['messages'] = []
request.session.save()
return messages

View File

@ -4,6 +4,12 @@ body {
font-family: sans-serif;
padding:none;
margin:0px;
height:100%;
}
form {
margin:0px;
padding:0px;
}
/* Carter One font */
@ -38,6 +44,11 @@ label {
/* website structure */
.mediagoblin_body {
position:relative;
min-height:100%;
}
.mediagoblin_header {
width:100%;
height:36px;
@ -46,6 +57,63 @@ label {
margin-bottom:40px;
}
.mediagoblin_footer {
width:100%;
height:26px;
background-color:#393939;
bottom:0px;
padding-top:8px;
position:absolute;
text-align:center;
font-size:14px;
color:#999;
}
.mediagoblin_content {
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;
@ -119,8 +187,8 @@ a.mediagoblin_logo:hover {
font-size:28px;
}
.form_field_input input {
width:300px;
.form_field_input input, .form_field_input textarea {
width:100%;
font-size:18px;
}
@ -159,11 +227,12 @@ ul.media_thumbnail {
li.media_thumbnail {
width:200px;
height:133px;
height:200px;
display:-moz-inline-stack;
display:inline-block;
vertical-align:top;
margin:0px 10px 10px 0px;
text-align:center;
zoom:1;
. *display:inline;
}

View File

@ -28,34 +28,49 @@
<body>
{% block mediagoblin_body %}
<div class="mediagoblin_body">
{% block mediagoblin_header %}
<div class="mediagoblin_header">
<div class="container_12">
<div class="grid_12">
{% block mediagoblin_logo %}
<a class="mediagoblin_logo" href="{{ request.urlgen('index') }}"></a>
{% endblock %}{% block mediagoblin_header_title %}{% endblock %}
<div class="mediagoblin_header_right">
{% if request.user %}
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
user= request.user['username']) }}">
{{ request.user['username'] }}</a>'s account
(<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">logout</a>)
{% else %}
<a href="{{ request.urlgen('mediagoblin.auth.login') }}">
Login</a>
{% endif %}
<div class="mediagoblin_header">
<div class="container_12">
<div class="grid_12">
{% block mediagoblin_logo %}
<a class="mediagoblin_logo" href="{{ request.urlgen('index') }}"></a>
{% endblock %}{% block mediagoblin_header_title %}{% endblock %}
<div class="mediagoblin_header_right">
{% if request.user %}
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
user= request.user['username']) }}">
{{ request.user['username'] }}</a>'s account
(<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">logout</a>)
{% else %}
<a href="{{ request.urlgen('mediagoblin.auth.login') }}">
Login</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<div class="container_12">
{% include "mediagoblin/utils/messages.html" %}
<div class="container_12 mediagoblin_content">
<div class="grid_12">
{% block mediagoblin_content %}
{% endblock mediagoblin_content %}
</div>
</div>
{% block mediagoblin_footer %}
<div class="mediagoblin_footer">
<div class="container_12">
<div class="grid_12">
Powered by <a href="http://mediagoblin.org">MediaGoblin</a>, a <a href="http://gnu.org/">GNU project</a>
</div>
</div>
</div>
{% endblock %}
{% endblock mediagoblin_body %}
</div>
</body>
</html>

View File

@ -27,13 +27,15 @@
method="POST" enctype="multipart/form-data">
<div class="grid_6 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(
media['media_files']['thumb']) }}" />
</div>
{{ wtforms_util.render_divs(form) }}
<div class="form_submit_buttons">
<a href="{{ media.url_for_self(request.urlgen) }}">Cancel</a>
<input type="submit" value="Save changes" class="button" />
</div>
<img src="{{ request.app.public_store.file_url(
media['media_files']['thumb']) }}" />
</div>
</form>

View File

@ -25,7 +25,9 @@
method="POST" enctype="multipart/form-data">
<div class="grid_6 prefix_1 suffix_1 form_box">
<h1>Submit yer media</h1>
{{ wtforms_util.render_divs(submit_form) }}
{{ 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>

View File

@ -20,15 +20,16 @@
{% 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><a href="{{ request.urlgen(
'mediagoblin.user_pages.user_home',
user=user.username) }}">{{ user.username }}</a>'s media</h1>
<h1>
<a href="{{ request.urlgen(
'mediagoblin.user_pages.user_home',
user=user.username) }}">{{ user.username }}</a>'s media</h1>
{% include "mediagoblin/utils/object_gallery.html" %}

View File

@ -25,16 +25,18 @@
</h1>
<img class="media_image" src="{{ request.app.public_store.file_url(
media.media_files.main) }}" />
<p>
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>
<p>{{ media.description_html }}</p>
{% endautoescape %}
<p>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>
{% if media['uploader'] == request.user['_id'] %}
<p><a href="{{ request.urlgen('mediagoblin.edit.edit_media',
user= media.uploader().username,

View File

@ -0,0 +1,32 @@
{#
# 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/>.
#}
{# Display any queued messages #}
{% set messages = fetch_messages(request) %}
{% if messages %}
<div class="container_12 mediagoblin_messages">
<div class="grid_12">
<ul class="mediagoblin_messages">
{% for msg in messages %}
<li class="message_{{ msg.level }}">{{ msg.text }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}

View File

@ -16,23 +16,47 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{# Generically render a field #}
{% macro render_field_div(field) %}
<div class="form_field_box">
<div class="form_field_label">{{ field.label }}</div>
{% if field.description -%}
<div class="form_field_description">{{ field.description }}</div>
{%- endif %}
<div class="form_field_input">{{ field }}</div>
{%- if field.errors -%}
{% for error in field.errors %}
<div class="form_field_error">
{{ error }}
</div>
{% endfor %}
{%- endif %}
</div>
{%- endmacro %}
{# Generically render a textarea
# ... mostly the same thing except it includes rows and cols #}
{% macro render_textarea_div(field, rows=8, cols=20) %}
<div class="form_field_box">
<div class="form_field_label">{{ field.label }}</div>
{% if field.description -%}
<div class="form_field_description">{{ field.description }}</div>
{%- endif %}
<div class="form_field_input">{{ field(rows=rows, cols=cols) }}</div>
{%- if field.errors -%}
{% for error in field.errors %}
<div class="form_field_error">
{{ error }}
</div>
{% endfor %}
{%- endif %}
</div>
{%- endmacro %}
{# Auto-render a form as a series of divs #}
{% macro render_divs(form) -%}
{% for field in form %}
<div class="form_field_box">
<div class="form_field_label">{{ field.label }}</div>
{% if field.description -%}
<div class="form_field_description">{{ field.description }}</div>
{%- endif %}
<div class="form_field_input">{{ field }}</div>
{%- if field.errors -%}
{% for error in field.errors %}
<div class="form_field_error">
{{ error }}
</div>
{% endfor %}
{%- endif %}
</div>
{{ render_field_div(field) }}
{% endfor %}
{%- endmacro %}

View File

@ -0,0 +1,44 @@
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011 Free Software Foundation, Inc
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from mediagoblin.messages import fetch_messages, add_message
from mediagoblin.tests.tools import setup_fresh_app
from mediagoblin import util
@setup_fresh_app
def test_messages(test_app):
"""
Added messages should show up in the request.session,
fetched messages should be the same as the added ones,
and fetching should clear the message list.
"""
# Aquire a request object
test_app.get('/')
context = util.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
request = context['request']
# The message queue should be empty
assert request.session.get('messages', []) == []
# Adding a message should modify the session accordingly
add_message(request, 'herp_derp', 'First!')
test_msg_queue = [{'text': 'First!', 'level': 'herp_derp'}]
assert request.session['messages'] == test_msg_queue
# fetch_messages should return and empty the queue
assert fetch_messages(request) == test_msg_queue
assert request.session.get('messages') == []

View File

@ -7,6 +7,9 @@ email_sender_address = "notice@mediagoblin.example.org"
email_debug_mode = true
db_name = __mediagoblin_tests__
# Celery shouldn't be set up by the paste app factory as it's set up
# elsewhere
# Celery shouldn't be set up by the application as it's setup via
# mediagoblin.celery_setup.from_celery
celery_setup_elsewhere = true
[celery]
celery_always_eager = true

View File

@ -21,9 +21,8 @@ import os, shutil
from paste.deploy import loadapp
from webtest import TestApp
from mediagoblin import util, mg_globals
from mediagoblin import util
from mediagoblin.config import read_mediagoblin_config
from mediagoblin.celery_setup import setup_celery_from_config
from mediagoblin.decorators import _make_safe
from mediagoblin.db.open import setup_connection_and_db_from_config
@ -36,7 +35,6 @@ TEST_APP_CONFIG = pkg_resources.resource_filename(
TEST_USER_DEV = pkg_resources.resource_filename(
'mediagoblin.tests', 'test_user_dev')
MGOBLIN_APP = None
CELERY_SETUP = False
USER_DEV_DIRECTORIES_TO_SETUP = [
'media/public', 'media/queue',
@ -60,8 +58,10 @@ def suicide_if_bad_celery_environ():
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
global MGOBLIN_APP
global CELERY_SETUP
# Just return the old app if that exists and it's okay to set up
# and return
@ -103,13 +103,6 @@ def get_test_app(dump_old_app=True):
app = TestApp(test_app)
MGOBLIN_APP = app
# setup celery
if not CELERY_SETUP:
setup_celery_from_config(
mg_globals.app_config, mg_globals.global_config,
set_environ=True)
CELERY_SETUP = True
return app

View File

@ -32,6 +32,7 @@ from lxml.html.clean import Cleaner
import markdown
from mediagoblin import mg_globals
from mediagoblin import messages
from mediagoblin.db.util import ObjectId
TESTS_ENABLED = False
@ -104,6 +105,10 @@ def get_jinja_env(template_loader, locale):
mg_globals.translations.gettext,
mg_globals.translations.ngettext)
# All templates will know how to ...
# ... fetch all waiting messages and remove them from the queue
template_env.globals['fetch_messages'] = messages.fetch_messages
if exists(locale):
SETUP_JINJA_ENVS[locale] = template_env